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.config.server.ConfigurationChangeListener; 043import org.forgerock.opendj.ldap.AVA; 044import org.forgerock.opendj.ldap.DN; 045import org.forgerock.opendj.ldap.RDN; 046import org.forgerock.opendj.ldap.ResultCode; 047import org.forgerock.opendj.ldap.SearchScope; 048import org.forgerock.opendj.ldap.schema.AttributeType; 049import org.forgerock.opendj.server.config.server.CertificateMapperCfg; 050import org.forgerock.opendj.server.config.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 mappings between certificate attribute names and user attribute types. */ 081 private LinkedHashMap<String,AttributeType> attributeMap; 082 /** The current configuration for this certificate mapper. */ 083 private SubjectAttributeToUserAttributeCertificateMapperCfg currentConfig; 084 /** The set of attributes to return in search result entries. */ 085 private LinkedHashSet<String> requestedAttributes; 086 087 /** 088 * Creates a new instance of this certificate mapper. Note that all actual 089 * initialization should be done in the 090 * <CODE>initializeCertificateMapper</CODE> method. 091 */ 092 public SubjectAttributeToUserAttributeCertificateMapper() 093 { 094 super(); 095 } 096 097 @Override 098 public void initializeCertificateMapper( 099 SubjectAttributeToUserAttributeCertificateMapperCfg configuration) 100 throws ConfigException, InitializationException 101 { 102 configuration.addSubjectAttributeToUserAttributeChangeListener(this); 103 104 currentConfig = configuration; 105 106 // Get and validate the subject attribute to user attribute mappings. 107 ConfigChangeResult ccr = new ConfigChangeResult(); 108 attributeMap = buildAttributeMap(configuration, ccr); 109 List<LocalizableMessage> messages = ccr.getMessages(); 110 if (!messages.isEmpty()) 111 { 112 throw new ConfigException(messages.iterator().next()); 113 } 114 115 // Make sure that all the user attributes are configured with equality 116 // indexes in all appropriate backends. 117 Set<DN> cfgBaseDNs = getUserBaseDNs(configuration); 118 for (DN baseDN : cfgBaseDNs) 119 { 120 for (AttributeType t : attributeMap.values()) 121 { 122 Backend<?> b = DirectoryServer.getBackend(baseDN); 123 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 124 { 125 logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(), 126 t.getNameOrOID(), b.getBackendID()); 127 } 128 } 129 } 130 131 // Create the attribute list to include in search requests. We want to 132 // include all user and operational attributes. 133 requestedAttributes = newLinkedHashSet("*", "+"); 134 } 135 136 @Override 137 public void finalizeCertificateMapper() 138 { 139 currentConfig.removeSubjectAttributeToUserAttributeChangeListener(this); 140 } 141 142 @Override 143 public Entry mapCertificateToUser(Certificate[] certificateChain) 144 throws DirectoryException 145 { 146 SubjectAttributeToUserAttributeCertificateMapperCfg config = currentConfig; 147 LinkedHashMap<String,AttributeType> theAttributeMap = this.attributeMap; 148 149 // Make sure that a peer certificate was provided. 150 if (certificateChain == null || certificateChain.length == 0) 151 { 152 LocalizableMessage message = ERR_SATUACM_NO_PEER_CERTIFICATE.get(); 153 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 154 } 155 156 // Get the first certificate in the chain. It must be an X.509 certificate. 157 X509Certificate peerCertificate; 158 try 159 { 160 peerCertificate = (X509Certificate) certificateChain[0]; 161 } 162 catch (ClassCastException e) 163 { 164 logger.traceException(e); 165 166 LocalizableMessage message = ERR_SATUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType()); 167 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 168 } 169 170 // Get the subject from the peer certificate and use it to create a search filter 171 DN peerDN; 172 X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal(); 173 String peerName = peerPrincipal.getName(X500Principal.RFC2253); 174 try 175 { 176 peerDN = DN.valueOf(peerName); 177 } 178 catch (LocalizedIllegalArgumentException de) 179 { 180 LocalizableMessage message = ERR_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN.get( 181 peerName, de.getMessageObject()); 182 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message, de); 183 } 184 185 LinkedList<SearchFilter> filterComps = new LinkedList<>(); 186 for (RDN rdn : peerDN) 187 { 188 for (AVA ava : rdn) 189 { 190 String lowerName = normalizeAttributeName(ava.getAttributeName()); 191 AttributeType attrType = theAttributeMap.get(lowerName); 192 if (attrType != null) 193 { 194 filterComps.add(SearchFilter.createEqualityFilter(attrType, ava.getAttributeValue())); 195 } 196 } 197 } 198 199 if (filterComps.isEmpty()) 200 { 201 LocalizableMessage message = ERR_SATUACM_NO_MAPPABLE_ATTRIBUTES.get(peerDN); 202 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 203 } 204 205 SearchFilter filter = SearchFilter.createANDFilter(filterComps); 206 Collection<DN> baseDNs = getUserBaseDNs(config); 207 208 // For each base DN, issue an internal search in an attempt to map the certificate. 209 Entry userEntry = null; 210 InternalClientConnection conn = getRootConnection(); 211 for (DN baseDN : baseDNs) 212 { 213 final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter) 214 .setSizeLimit(1) 215 .setTimeLimit(10) 216 .addAttribute(requestedAttributes); 217 InternalSearchOperation searchOperation = conn.processSearch(request); 218 219 switch (searchOperation.getResultCode().asEnum()) 220 { 221 case SUCCESS: 222 // This is fine. No action needed. 223 break; 224 225 case NO_SUCH_OBJECT: 226 // The search base doesn't exist. Not an ideal situation, but we'll 227 // ignore it. 228 break; 229 230 case SIZE_LIMIT_EXCEEDED: 231 // Multiple entries matched the filter. This is not acceptable. 232 LocalizableMessage message = ERR_SATUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(peerDN); 233 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 234 235 case TIME_LIMIT_EXCEEDED: 236 case ADMIN_LIMIT_EXCEEDED: 237 // The search criteria was too inefficient. 238 message = ERR_SATUACM_INEFFICIENT_SEARCH.get(peerDN, searchOperation.getErrorMessage()); 239 throw new DirectoryException(searchOperation.getResultCode(), message); 240 241 default: 242 // Just pass on the failure that was returned for this search. 243 message = ERR_SATUACM_SEARCH_FAILED.get(peerDN, searchOperation.getErrorMessage()); 244 throw new DirectoryException(searchOperation.getResultCode(), message); 245 } 246 247 for (SearchResultEntry entry : searchOperation.getSearchEntries()) 248 { 249 if (userEntry == null) 250 { 251 userEntry = entry; 252 } 253 else 254 { 255 LocalizableMessage message = ERR_SATUACM_MULTIPLE_MATCHING_ENTRIES. 256 get(peerDN, userEntry.getName(), entry.getName()); 257 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 258 } 259 } 260 } 261 262 // We either found exactly one user entry or we did not find any. 263 return userEntry; 264 } 265 266 @Override 267 public boolean isConfigurationAcceptable(CertificateMapperCfg configuration, 268 List<LocalizableMessage> unacceptableReasons) 269 { 270 SubjectAttributeToUserAttributeCertificateMapperCfg config = 271 (SubjectAttributeToUserAttributeCertificateMapperCfg) configuration; 272 return isConfigurationChangeAcceptable(config, unacceptableReasons); 273 } 274 275 @Override 276 public boolean isConfigurationChangeAcceptable( 277 SubjectAttributeToUserAttributeCertificateMapperCfg configuration, 278 List<LocalizableMessage> unacceptableReasons) 279 { 280 ConfigChangeResult ccr = new ConfigChangeResult(); 281 buildAttributeMap(configuration, ccr); 282 unacceptableReasons.addAll(ccr.getMessages()); 283 return ResultCode.SUCCESS.equals(ccr.getResultCode()); 284 } 285 286 @Override 287 public ConfigChangeResult applyConfigurationChange(SubjectAttributeToUserAttributeCertificateMapperCfg configuration) 288 { 289 final ConfigChangeResult ccr = new ConfigChangeResult(); 290 LinkedHashMap<String, AttributeType> newAttributeMap = buildAttributeMap(configuration, ccr); 291 292 // Make sure that all the user attributes are configured with equality 293 // indexes in all appropriate backends. 294 Set<DN> cfgBaseDNs = getUserBaseDNs(configuration); 295 for (DN baseDN : cfgBaseDNs) 296 { 297 for (AttributeType t : newAttributeMap.values()) 298 { 299 Backend<?> b = DirectoryServer.getBackend(baseDN); 300 if (b != null && !b.isIndexed(t, IndexType.EQUALITY)) 301 { 302 LocalizableMessage message = 303 WARN_SATUACM_ATTR_UNINDEXED.get(configuration.dn(), t.getNameOrOID(), b.getBackendID()); 304 ccr.addMessage(message); 305 logger.error(message); 306 } 307 } 308 } 309 310 if (ccr.getResultCode() == ResultCode.SUCCESS) 311 { 312 attributeMap = newAttributeMap; 313 currentConfig = configuration; 314 } 315 316 return ccr; 317 } 318 319 /** 320 * If we have an explicit set of base DNs, then use it. 321 * Otherwise, use the set of public naming contexts in the server. 322 */ 323 private Set<DN> getUserBaseDNs(SubjectAttributeToUserAttributeCertificateMapperCfg config) 324 { 325 Set<DN> baseDNs = config.getUserBaseDN(); 326 if (baseDNs == null || baseDNs.isEmpty()) 327 { 328 baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 329 } 330 return baseDNs; 331 } 332 333 /** Get and validate the subject attribute to user attribute mappings. */ 334 private LinkedHashMap<String, AttributeType> buildAttributeMap( 335 SubjectAttributeToUserAttributeCertificateMapperCfg cfg, ConfigChangeResult ccr) 336 { 337 LinkedHashMap<String, AttributeType> results = new LinkedHashMap<>(); 338 for (String mapStr : cfg.getSubjectAttributeMapping()) 339 { 340 String lowerMap = toLowerCase(mapStr); 341 int colonPos = lowerMap.indexOf(':'); 342 if (colonPos <= 0) 343 { 344 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 345 ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfg.dn(), mapStr)); 346 return null; 347 } 348 349 String certAttrName = lowerMap.substring(0, colonPos).trim(); 350 String userAttrName = lowerMap.substring(colonPos+1).trim(); 351 if (certAttrName.length() == 0 || userAttrName.length() == 0) 352 { 353 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 354 ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfg.dn(), mapStr)); 355 return null; 356 } 357 358 // Try to normalize the provided certAttrName 359 certAttrName = normalizeAttributeName(certAttrName); 360 if (results.containsKey(certAttrName)) 361 { 362 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 363 ccr.addMessage(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(cfg.dn(), certAttrName)); 364 return null; 365 } 366 367 AttributeType userAttrType = DirectoryServer.getSchema().getAttributeType(userAttrName); 368 if (userAttrType.isPlaceHolder()) 369 { 370 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 371 ccr.addMessage(ERR_SATUACM_NO_SUCH_ATTR.get(mapStr, cfg.dn(), userAttrName)); 372 return null; 373 } 374 if (results.values().contains(userAttrType)) 375 { 376 ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION); 377 ccr.addMessage(ERR_SATUACM_DUPLICATE_USER_ATTR.get(cfg.dn(), userAttrType.getNameOrOID())); 378 return null; 379 } 380 381 results.put(certAttrName, userAttrType); 382 } 383 return results; 384 } 385 386 private static String normalizeAttributeName(String attrName) 387 { 388 return toLowerCase(DirectoryServer.getSchema().getAttributeType(attrName).getNameOrOID()); 389 } 390}