001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2007-2008 Sun Microsystems, Inc. 025 * Portions Copyright 2012-2015 ForgeRock AS 026 * Portions Copyright 2013 Manuel Gaupp 027 */ 028package org.opends.server.extensions; 029 030import static org.opends.messages.ExtensionMessages.*; 031import static org.opends.server.protocols.internal.InternalClientConnection.*; 032import static org.opends.server.protocols.internal.Requests.*; 033import static org.opends.server.util.CollectionUtils.*; 034import static org.opends.server.util.StaticUtils.*; 035 036import java.security.cert.Certificate; 037import java.security.cert.X509Certificate; 038import java.util.Collection; 039import java.util.LinkedHashMap; 040import java.util.LinkedHashSet; 041import java.util.LinkedList; 042import java.util.List; 043import java.util.Set; 044 045import javax.security.auth.x500.X500Principal; 046 047import org.forgerock.i18n.LocalizableMessage; 048import org.forgerock.i18n.slf4j.LocalizedLogger; 049import org.forgerock.opendj.config.server.ConfigChangeResult; 050import org.forgerock.opendj.config.server.ConfigException; 051import org.forgerock.opendj.ldap.ResultCode; 052import org.forgerock.opendj.ldap.SearchScope; 053import org.opends.server.admin.server.ConfigurationChangeListener; 054import org.opends.server.admin.std.server.CertificateMapperCfg; 055import org.opends.server.admin.std.server.SubjectAttributeToUserAttributeCertificateMapperCfg; 056import org.opends.server.api.Backend; 057import org.opends.server.api.CertificateMapper; 058import org.opends.server.core.DirectoryServer; 059import org.opends.server.protocols.internal.InternalClientConnection; 060import org.opends.server.protocols.internal.InternalSearchOperation; 061import org.opends.server.protocols.internal.SearchRequest; 062import org.opends.server.types.AttributeType; 063import org.opends.server.types.DN; 064import org.opends.server.types.DirectoryException; 065import org.opends.server.types.Entry; 066import org.opends.server.types.IndexType; 067import org.opends.server.types.InitializationException; 068import org.opends.server.types.RDN; 069import org.opends.server.types.SearchFilter; 070import org.opends.server.types.SearchResultEntry; 071 072/** 073 * This class implements a very simple Directory Server certificate mapper that 074 * will map a certificate to a user based on attributes contained in both the 075 * certificate subject and the user's entry. The configuration may include 076 * mappings from certificate attributes to attributes in user entries, and all 077 * of those certificate attributes that are present in the subject will be used 078 * to search for matching user entries. 079 */ 080public class SubjectAttributeToUserAttributeCertificateMapper 081 extends CertificateMapper< 082 SubjectAttributeToUserAttributeCertificateMapperCfg> 083 implements ConfigurationChangeListener< 084 SubjectAttributeToUserAttributeCertificateMapperCfg> 085{ 086 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 087 088 /** The DN of the configuration entry for this certificate mapper. */ 089 private DN configEntryDN; 090 /** The mappings between certificate attribute names and user attribute types. */ 091 private LinkedHashMap<String,AttributeType> attributeMap; 092 /** The current configuration for this certificate mapper. */ 093 private SubjectAttributeToUserAttributeCertificateMapperCfg currentConfig; 094 /** The set of attributes to return in search result entries. */ 095 private LinkedHashSet<String> requestedAttributes; 096 097 098 /** 099 * Creates a new instance of this certificate mapper. Note that all actual 100 * initialization should be done in the 101 * <CODE>initializeCertificateMapper</CODE> method. 102 */ 103 public SubjectAttributeToUserAttributeCertificateMapper() 104 { 105 super(); 106 } 107 108 109 110 /** {@inheritDoc} */ 111 @Override 112 public void initializeCertificateMapper( 113 SubjectAttributeToUserAttributeCertificateMapperCfg configuration) 114 throws ConfigException, InitializationException 115 { 116 configuration.addSubjectAttributeToUserAttributeChangeListener(this); 117 118 currentConfig = configuration; 119 configEntryDN = configuration.dn(); 120 121 // Get and validate the subject attribute to user attribute mappings. 122 ConfigChangeResult ccr = new ConfigChangeResult(); 123 attributeMap = buildAttributeMap(configuration, configEntryDN, ccr); 124 List<LocalizableMessage> messages = ccr.getMessages(); 125 if (!messages.isEmpty()) 126 { 127 throw new ConfigException(messages.iterator().next()); 128 } 129 130 // Make sure that all the user attributes are configured with equality 131 // indexes in all appropriate backends. 132 Set<DN> cfgBaseDNs = getUserBaseDNs(configuration); 133 for (DN baseDN : cfgBaseDNs) 134 { 135 for (AttributeType t : attributeMap.values()) 136 { 137 Backend<?> b = DirectoryServer.getBackend(baseDN); 138 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 139 { 140 logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(), 141 t.getNameOrOID(), b.getBackendID()); 142 } 143 } 144 } 145 146 // Create the attribute list to include in search requests. We want to 147 // include all user and operational attributes. 148 requestedAttributes = newLinkedHashSet("*", "+"); 149 } 150 151 /** {@inheritDoc} */ 152 @Override 153 public void finalizeCertificateMapper() 154 { 155 currentConfig.removeSubjectAttributeToUserAttributeChangeListener(this); 156 } 157 158 159 160 /** {@inheritDoc} */ 161 @Override 162 public Entry mapCertificateToUser(Certificate[] certificateChain) 163 throws DirectoryException 164 { 165 SubjectAttributeToUserAttributeCertificateMapperCfg config = currentConfig; 166 LinkedHashMap<String,AttributeType> theAttributeMap = this.attributeMap; 167 168 169 // Make sure that a peer certificate was provided. 170 if (certificateChain == null || certificateChain.length == 0) 171 { 172 LocalizableMessage message = ERR_SATUACM_NO_PEER_CERTIFICATE.get(); 173 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 174 } 175 176 177 // Get the first certificate in the chain. It must be an X.509 certificate. 178 X509Certificate peerCertificate; 179 try 180 { 181 peerCertificate = (X509Certificate) certificateChain[0]; 182 } 183 catch (Exception e) 184 { 185 logger.traceException(e); 186 187 LocalizableMessage message = ERR_SATUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType()); 188 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 189 } 190 191 192 // Get the subject from the peer certificate and use it to create a search 193 // filter. 194 DN peerDN; 195 X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal(); 196 String peerName = peerPrincipal.getName(X500Principal.RFC2253); 197 try 198 { 199 peerDN = DN.valueOf(peerName); 200 } 201 catch (DirectoryException de) 202 { 203 LocalizableMessage message = ERR_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN.get( 204 peerName, de.getMessageObject()); 205 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message, de); 206 } 207 208 LinkedList<SearchFilter> filterComps = new LinkedList<>(); 209 for (int i=0; i < peerDN.size(); i++) 210 { 211 RDN rdn = peerDN.getRDN(i); 212 for (int j=0; j < rdn.getNumValues(); j++) 213 { 214 String lowerName = toLowerCase(rdn.getAttributeName(j)); 215 216 // Try to normalize lowerName 217 lowerName = normalizeAttributeName(lowerName); 218 219 AttributeType attrType = theAttributeMap.get(lowerName); 220 if (attrType != null) 221 { 222 filterComps.add(SearchFilter.createEqualityFilter(attrType, rdn.getAttributeValue(j))); 223 } 224 } 225 } 226 227 if (filterComps.isEmpty()) 228 { 229 LocalizableMessage message = ERR_SATUACM_NO_MAPPABLE_ATTRIBUTES.get(peerDN); 230 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 231 } 232 233 SearchFilter filter = SearchFilter.createANDFilter(filterComps); 234 Collection<DN> baseDNs = getUserBaseDNs(config); 235 236 // For each base DN, issue an internal search in an attempt to map the certificate. 237 Entry userEntry = null; 238 InternalClientConnection conn = getRootConnection(); 239 for (DN baseDN : baseDNs) 240 { 241 final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter) 242 .setSizeLimit(1) 243 .setTimeLimit(10) 244 .addAttribute(requestedAttributes); 245 InternalSearchOperation searchOperation = conn.processSearch(request); 246 247 switch (searchOperation.getResultCode().asEnum()) 248 { 249 case SUCCESS: 250 // This is fine. No action needed. 251 break; 252 253 case NO_SUCH_OBJECT: 254 // The search base doesn't exist. Not an ideal situation, but we'll 255 // ignore it. 256 break; 257 258 case SIZE_LIMIT_EXCEEDED: 259 // Multiple entries matched the filter. This is not acceptable. 260 LocalizableMessage message = ERR_SATUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(peerDN); 261 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 262 263 case TIME_LIMIT_EXCEEDED: 264 case ADMIN_LIMIT_EXCEEDED: 265 // The search criteria was too inefficient. 266 message = ERR_SATUACM_INEFFICIENT_SEARCH.get(peerDN, searchOperation.getErrorMessage()); 267 throw new DirectoryException(searchOperation.getResultCode(), message); 268 269 default: 270 // Just pass on the failure that was returned for this search. 271 message = ERR_SATUACM_SEARCH_FAILED.get(peerDN, searchOperation.getErrorMessage()); 272 throw new DirectoryException(searchOperation.getResultCode(), message); 273 } 274 275 for (SearchResultEntry entry : searchOperation.getSearchEntries()) 276 { 277 if (userEntry == null) 278 { 279 userEntry = entry; 280 } 281 else 282 { 283 LocalizableMessage message = ERR_SATUACM_MULTIPLE_MATCHING_ENTRIES. 284 get(peerDN, userEntry.getName(), entry.getName()); 285 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 286 } 287 } 288 } 289 290 291 // If we've gotten here, then we either found exactly one user entry or we 292 // didn't find any. Either way, return the entry or null to the caller. 293 return userEntry; 294 } 295 296 /** {@inheritDoc} */ 297 @Override 298 public boolean isConfigurationAcceptable(CertificateMapperCfg configuration, 299 List<LocalizableMessage> unacceptableReasons) 300 { 301 SubjectAttributeToUserAttributeCertificateMapperCfg config = 302 (SubjectAttributeToUserAttributeCertificateMapperCfg) configuration; 303 return isConfigurationChangeAcceptable(config, unacceptableReasons); 304 } 305 306 307 308 /** {@inheritDoc} */ 309 @Override 310 public boolean isConfigurationChangeAcceptable( 311 SubjectAttributeToUserAttributeCertificateMapperCfg configuration, 312 List<LocalizableMessage> unacceptableReasons) 313 { 314 ConfigChangeResult ccr = new ConfigChangeResult(); 315 buildAttributeMap(configuration, configuration.dn(), ccr); 316 unacceptableReasons.addAll(ccr.getMessages()); 317 return ResultCode.SUCCESS.equals(ccr.getResultCode()); 318 } 319 320 /** {@inheritDoc} */ 321 @Override 322 public ConfigChangeResult applyConfigurationChange(SubjectAttributeToUserAttributeCertificateMapperCfg configuration) 323 { 324 final ConfigChangeResult ccr = new ConfigChangeResult(); 325 LinkedHashMap<String, AttributeType> newAttributeMap = buildAttributeMap(configuration, configEntryDN, ccr); 326 327 // Make sure that all the user attributes are configured with equality 328 // indexes in all appropriate backends. 329 Set<DN> cfgBaseDNs = getUserBaseDNs(configuration); 330 for (DN baseDN : cfgBaseDNs) 331 { 332 for (AttributeType t : newAttributeMap.values()) 333 { 334 Backend<?> b = DirectoryServer.getBackend(baseDN); 335 if (b != null && !b.isIndexed(t, IndexType.EQUALITY)) 336 { 337 LocalizableMessage message = 338 WARN_SATUACM_ATTR_UNINDEXED.get(configuration.dn(), t.getNameOrOID(), b.getBackendID()); 339 ccr.addMessage(message); 340 logger.error(message); 341 } 342 } 343 } 344 345 if (ccr.getResultCode() == ResultCode.SUCCESS) 346 { 347 attributeMap = newAttributeMap; 348 currentConfig = configuration; 349 } 350 351 return ccr; 352 } 353 354 /** 355 * If we have an explicit set of base DNs, then use it. 356 * Otherwise, use the set of public naming contexts in the server. 357 */ 358 private Set<DN> getUserBaseDNs(SubjectAttributeToUserAttributeCertificateMapperCfg config) 359 { 360 Set<DN> baseDNs = config.getUserBaseDN(); 361 if (baseDNs == null || baseDNs.isEmpty()) 362 { 363 baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 364 } 365 return baseDNs; 366 } 367 368 /** Get and validate the subject attribute to user attribute mappings. */ 369 private LinkedHashMap<String, AttributeType> buildAttributeMap( 370 SubjectAttributeToUserAttributeCertificateMapperCfg configuration, DN cfgEntryDN, ConfigChangeResult ccr) 371 { 372 LinkedHashMap<String, AttributeType> results = new LinkedHashMap<>(); 373 for (String mapStr : configuration.getSubjectAttributeMapping()) 374 { 375 String lowerMap = toLowerCase(mapStr); 376 int colonPos = lowerMap.indexOf(':'); 377 if (colonPos <= 0) 378 { 379 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 380 ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfgEntryDN, mapStr)); 381 return null; 382 } 383 384 String certAttrName = lowerMap.substring(0, colonPos).trim(); 385 String userAttrName = lowerMap.substring(colonPos+1).trim(); 386 if (certAttrName.length() == 0 || userAttrName.length() == 0) 387 { 388 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 389 ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfgEntryDN, mapStr)); 390 return null; 391 } 392 393 // Try to normalize the provided certAttrName 394 certAttrName = normalizeAttributeName(certAttrName); 395 if (results.containsKey(certAttrName)) 396 { 397 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 398 ccr.addMessage(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(cfgEntryDN, certAttrName)); 399 return null; 400 } 401 402 AttributeType userAttrType = DirectoryServer.getAttributeTypeOrNull(userAttrName); 403 if (userAttrType == null) 404 { 405 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 406 ccr.addMessage(ERR_SATUACM_NO_SUCH_ATTR.get(mapStr, cfgEntryDN, userAttrName)); 407 return null; 408 } 409 if (results.values().contains(userAttrType)) 410 { 411 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 412 ccr.addMessage(ERR_SATUACM_DUPLICATE_USER_ATTR.get(cfgEntryDN, userAttrType.getNameOrOID())); 413 return null; 414 } 415 416 results.put(certAttrName, userAttrType); 417 } 418 return results; 419 } 420 421 422 423 /** 424 * Tries to normalize the given attribute name; if normalization is not 425 * possible the original String value is returned. 426 * 427 * @param attrName The attribute name which should be normalized. 428 * @return The normalized attribute name. 429 */ 430 private static String normalizeAttributeName(String attrName) 431 { 432 AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(attrName); 433 if (attrType != null) 434 { 435 String attrNameNormalized = attrType.getNormalizedPrimaryNameOrOID(); 436 if (attrNameNormalized != null) 437 { 438 return attrNameNormalized; 439 } 440 } 441 return attrName; 442 } 443}