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 */ 027package org.opends.server.extensions; 028 029import java.security.cert.Certificate; 030import java.security.cert.X509Certificate; 031import java.util.Collection; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Set; 035 036import javax.security.auth.x500.X500Principal; 037 038import org.forgerock.i18n.LocalizableMessage; 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.ByteString; 043import org.forgerock.opendj.ldap.ResultCode; 044import org.forgerock.opendj.ldap.SearchScope; 045import org.opends.server.admin.server.ConfigurationChangeListener; 046import org.opends.server.admin.std.server.CertificateMapperCfg; 047import org.opends.server.admin.std.server.SubjectDNToUserAttributeCertificateMapperCfg; 048import org.opends.server.api.Backend; 049import org.opends.server.api.CertificateMapper; 050import org.opends.server.core.DirectoryServer; 051import org.opends.server.protocols.internal.InternalClientConnection; 052import org.opends.server.protocols.internal.InternalSearchOperation; 053import org.opends.server.protocols.internal.SearchRequest; 054import org.opends.server.types.*; 055 056import static org.opends.messages.ExtensionMessages.*; 057import static org.opends.server.protocols.internal.InternalClientConnection.*; 058import static org.opends.server.protocols.internal.Requests.*; 059import static org.opends.server.util.CollectionUtils.*; 060 061/** 062 * This class implements a very simple Directory Server certificate mapper that 063 * will map a certificate to a user only if that user's entry contains an 064 * attribute with the subject of the client certificate. There must be exactly 065 * one matching user entry for the mapping to be successful. 066 */ 067public class SubjectDNToUserAttributeCertificateMapper 068 extends CertificateMapper< 069 SubjectDNToUserAttributeCertificateMapperCfg> 070 implements ConfigurationChangeListener< 071 SubjectDNToUserAttributeCertificateMapperCfg> 072{ 073 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 074 075 /** The DN of the configuration entry for this certificate mapper. */ 076 private DN configEntryDN; 077 078 /** The current configuration for this certificate mapper. */ 079 private SubjectDNToUserAttributeCertificateMapperCfg currentConfig; 080 081 /** The set of attributes to return in search result entries. */ 082 private LinkedHashSet<String> requestedAttributes; 083 084 085 /** 086 * Creates a new instance of this certificate mapper. Note that all actual 087 * initialization should be done in the 088 * <CODE>initializeCertificateMapper</CODE> method. 089 */ 090 public SubjectDNToUserAttributeCertificateMapper() 091 { 092 super(); 093 } 094 095 096 097 /** {@inheritDoc} */ 098 @Override 099 public void initializeCertificateMapper( 100 SubjectDNToUserAttributeCertificateMapperCfg 101 configuration) 102 throws ConfigException, InitializationException 103 { 104 configuration.addSubjectDNToUserAttributeChangeListener(this); 105 106 currentConfig = configuration; 107 configEntryDN = configuration.dn(); 108 109 110 // Make sure that the subject attribute is configured for equality in all 111 // appropriate backends. 112 Set<DN> cfgBaseDNs = configuration.getUserBaseDN(); 113 if (cfgBaseDNs == null || cfgBaseDNs.isEmpty()) 114 { 115 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 116 } 117 118 AttributeType t = configuration.getSubjectAttribute(); 119 for (DN baseDN : cfgBaseDNs) 120 { 121 Backend b = DirectoryServer.getBackend(baseDN); 122 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 123 { 124 logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(), 125 t.getNameOrOID(), b.getBackendID()); 126 } 127 } 128 129 // Create the attribute list to include in search requests. We want to 130 // include all user and operational attributes. 131 requestedAttributes = newLinkedHashSet("*", "+"); 132 } 133 134 135 136 /** {@inheritDoc} */ 137 @Override 138 public void finalizeCertificateMapper() 139 { 140 currentConfig.removeSubjectDNToUserAttributeChangeListener(this); 141 } 142 143 144 145 /** {@inheritDoc} */ 146 @Override 147 public Entry mapCertificateToUser(Certificate[] certificateChain) 148 throws DirectoryException 149 { 150 SubjectDNToUserAttributeCertificateMapperCfg config = 151 currentConfig; 152 AttributeType subjectAttributeType = config.getSubjectAttribute(); 153 154 155 // Make sure that a peer certificate was provided. 156 if (certificateChain == null || certificateChain.length == 0) 157 { 158 LocalizableMessage message = ERR_SDTUACM_NO_PEER_CERTIFICATE.get(); 159 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 160 } 161 162 163 // Get the first certificate in the chain. It must be an X.509 certificate. 164 X509Certificate peerCertificate; 165 try 166 { 167 peerCertificate = (X509Certificate) certificateChain[0]; 168 } 169 catch (Exception e) 170 { 171 logger.traceException(e); 172 173 LocalizableMessage message = ERR_SDTUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType()); 174 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 175 } 176 177 178 // Get the subject from the peer certificate and use it to create a search 179 // filter. 180 X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal(); 181 String peerName = peerPrincipal.getName(X500Principal.RFC2253); 182 SearchFilter filter = SearchFilter.createEqualityFilter( 183 subjectAttributeType, ByteString.valueOfUtf8(peerName)); 184 185 186 // If we have an explicit set of base DNs, then use it. Otherwise, use the 187 // set of public naming contexts in the server. 188 Collection<DN> baseDNs = config.getUserBaseDN(); 189 if (baseDNs == null || baseDNs.isEmpty()) 190 { 191 baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 192 } 193 194 195 // For each base DN, issue an internal search in an attempt to map the 196 // certificate. 197 Entry userEntry = null; 198 InternalClientConnection conn = getRootConnection(); 199 for (DN baseDN : baseDNs) 200 { 201 final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter) 202 .setSizeLimit(1) 203 .setTimeLimit(10) 204 .addAttribute(requestedAttributes); 205 InternalSearchOperation searchOperation = conn.processSearch(request); 206 switch (searchOperation.getResultCode().asEnum()) 207 { 208 case SUCCESS: 209 // This is fine. No action needed. 210 break; 211 212 case NO_SUCH_OBJECT: 213 // The search base doesn't exist. Not an ideal situation, but we'll 214 // ignore it. 215 break; 216 217 case SIZE_LIMIT_EXCEEDED: 218 // Multiple entries matched the filter. This is not acceptable. 219 LocalizableMessage message = ERR_SDTUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get( 220 peerName); 221 throw new DirectoryException( 222 ResultCode.INVALID_CREDENTIALS, message); 223 224 225 case TIME_LIMIT_EXCEEDED: 226 case ADMIN_LIMIT_EXCEEDED: 227 // The search criteria was too inefficient. 228 message = ERR_SDTUACM_INEFFICIENT_SEARCH.get(peerName, searchOperation.getErrorMessage()); 229 throw new DirectoryException(searchOperation.getResultCode(), message); 230 231 default: 232 // Just pass on the failure that was returned for this search. 233 message = ERR_SDTUACM_SEARCH_FAILED.get(peerName, searchOperation.getErrorMessage()); 234 throw new DirectoryException(searchOperation.getResultCode(), message); 235 } 236 237 for (SearchResultEntry entry : searchOperation.getSearchEntries()) 238 { 239 if (userEntry == null) 240 { 241 userEntry = entry; 242 } 243 else 244 { 245 LocalizableMessage message = ERR_SDTUACM_MULTIPLE_MATCHING_ENTRIES. 246 get(peerName, userEntry.getName(), entry.getName()); 247 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 248 } 249 } 250 } 251 252 253 // If we've gotten here, then we either found exactly one user entry or we 254 // didn't find any. Either way, return the entry or null to the caller. 255 return userEntry; 256 } 257 258 259 260 /** {@inheritDoc} */ 261 @Override 262 public boolean isConfigurationAcceptable(CertificateMapperCfg configuration, 263 List<LocalizableMessage> unacceptableReasons) 264 { 265 SubjectDNToUserAttributeCertificateMapperCfg config = 266 (SubjectDNToUserAttributeCertificateMapperCfg) configuration; 267 return isConfigurationChangeAcceptable(config, unacceptableReasons); 268 } 269 270 271 272 /** {@inheritDoc} */ 273 @Override 274 public boolean isConfigurationChangeAcceptable( 275 SubjectDNToUserAttributeCertificateMapperCfg 276 configuration, 277 List<LocalizableMessage> unacceptableReasons) 278 { 279 return true; 280 } 281 282 283 284 /** {@inheritDoc} */ 285 @Override 286 public ConfigChangeResult applyConfigurationChange( 287 SubjectDNToUserAttributeCertificateMapperCfg 288 configuration) 289 { 290 currentConfig = configuration; 291 return new ConfigChangeResult(); 292 } 293} 294