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