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