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.forgerock.opendj.config.server.ConfigurationChangeListener; 038import org.forgerock.opendj.server.config.server.CertificateMapperCfg; 039import org.forgerock.opendj.server.config.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 * Creates a new instance of this certificate mapper. Note that all actual 078 * initialization should be done in the 079 * <CODE>initializeCertificateMapper</CODE> method. 080 */ 081 public SubjectDNToUserAttributeCertificateMapper() 082 { 083 super(); 084 } 085 086 @Override 087 public void initializeCertificateMapper( 088 SubjectDNToUserAttributeCertificateMapperCfg 089 configuration) 090 throws ConfigException, InitializationException 091 { 092 configuration.addSubjectDNToUserAttributeChangeListener(this); 093 094 currentConfig = configuration; 095 configEntryDN = configuration.dn(); 096 097 // Make sure that the subject attribute is configured for equality in all 098 // appropriate backends. 099 Set<DN> cfgBaseDNs = configuration.getUserBaseDN(); 100 if (cfgBaseDNs == null || cfgBaseDNs.isEmpty()) 101 { 102 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 103 } 104 105 AttributeType t = configuration.getSubjectAttribute(); 106 for (DN baseDN : cfgBaseDNs) 107 { 108 Backend b = DirectoryServer.getBackend(baseDN); 109 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 110 { 111 logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(), 112 t.getNameOrOID(), b.getBackendID()); 113 } 114 } 115 116 // Create the attribute list to include in search requests. We want to 117 // include all user and operational attributes. 118 requestedAttributes = newLinkedHashSet("*", "+"); 119 } 120 121 @Override 122 public void finalizeCertificateMapper() 123 { 124 currentConfig.removeSubjectDNToUserAttributeChangeListener(this); 125 } 126 127 @Override 128 public Entry mapCertificateToUser(Certificate[] certificateChain) 129 throws DirectoryException 130 { 131 SubjectDNToUserAttributeCertificateMapperCfg config = 132 currentConfig; 133 AttributeType subjectAttributeType = config.getSubjectAttribute(); 134 135 // Make sure that a peer certificate was provided. 136 if (certificateChain == null || certificateChain.length == 0) 137 { 138 LocalizableMessage message = ERR_SDTUACM_NO_PEER_CERTIFICATE.get(); 139 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 140 } 141 142 // Get the first certificate in the chain. It must be an X.509 certificate. 143 X509Certificate peerCertificate; 144 try 145 { 146 peerCertificate = (X509Certificate) certificateChain[0]; 147 } 148 catch (Exception e) 149 { 150 logger.traceException(e); 151 152 LocalizableMessage message = ERR_SDTUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType()); 153 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 154 } 155 156 // Get the subject from the peer certificate and use it to create a search 157 // filter. 158 X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal(); 159 String peerName = peerPrincipal.getName(X500Principal.RFC2253); 160 SearchFilter filter = SearchFilter.createEqualityFilter( 161 subjectAttributeType, ByteString.valueOfUtf8(peerName)); 162 163 // If we have an explicit set of base DNs, then use it. Otherwise, use the 164 // set of public naming contexts in the server. 165 Collection<DN> baseDNs = config.getUserBaseDN(); 166 if (baseDNs == null || baseDNs.isEmpty()) 167 { 168 baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 169 } 170 171 // For each base DN, issue an internal search in an attempt to map the 172 // certificate. 173 Entry userEntry = null; 174 InternalClientConnection conn = getRootConnection(); 175 for (DN baseDN : baseDNs) 176 { 177 final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter) 178 .setSizeLimit(1) 179 .setTimeLimit(10) 180 .addAttribute(requestedAttributes); 181 InternalSearchOperation searchOperation = conn.processSearch(request); 182 switch (searchOperation.getResultCode().asEnum()) 183 { 184 case SUCCESS: 185 // This is fine. No action needed. 186 break; 187 188 case NO_SUCH_OBJECT: 189 // The search base doesn't exist. Not an ideal situation, but we'll 190 // ignore it. 191 break; 192 193 case SIZE_LIMIT_EXCEEDED: 194 // Multiple entries matched the filter. This is not acceptable. 195 LocalizableMessage message = ERR_SDTUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get( 196 peerName); 197 throw new DirectoryException( 198 ResultCode.INVALID_CREDENTIALS, message); 199 200 case TIME_LIMIT_EXCEEDED: 201 case ADMIN_LIMIT_EXCEEDED: 202 // The search criteria was too inefficient. 203 message = ERR_SDTUACM_INEFFICIENT_SEARCH.get(peerName, searchOperation.getErrorMessage()); 204 throw new DirectoryException(searchOperation.getResultCode(), message); 205 206 default: 207 // Just pass on the failure that was returned for this search. 208 message = ERR_SDTUACM_SEARCH_FAILED.get(peerName, searchOperation.getErrorMessage()); 209 throw new DirectoryException(searchOperation.getResultCode(), message); 210 } 211 212 for (SearchResultEntry entry : searchOperation.getSearchEntries()) 213 { 214 if (userEntry == null) 215 { 216 userEntry = entry; 217 } 218 else 219 { 220 LocalizableMessage message = ERR_SDTUACM_MULTIPLE_MATCHING_ENTRIES. 221 get(peerName, userEntry.getName(), entry.getName()); 222 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 223 } 224 } 225 } 226 227 // If we've gotten here, then we either found exactly one user entry or we 228 // didn't find any. Either way, return the entry or null to the caller. 229 return userEntry; 230 } 231 232 @Override 233 public boolean isConfigurationAcceptable(CertificateMapperCfg configuration, 234 List<LocalizableMessage> unacceptableReasons) 235 { 236 SubjectDNToUserAttributeCertificateMapperCfg config = 237 (SubjectDNToUserAttributeCertificateMapperCfg) configuration; 238 return isConfigurationChangeAcceptable(config, unacceptableReasons); 239 } 240 241 @Override 242 public boolean isConfigurationChangeAcceptable( 243 SubjectDNToUserAttributeCertificateMapperCfg 244 configuration, 245 List<LocalizableMessage> unacceptableReasons) 246 { 247 return true; 248 } 249 250 @Override 251 public ConfigChangeResult applyConfigurationChange( 252 SubjectDNToUserAttributeCertificateMapperCfg 253 configuration) 254 { 255 currentConfig = configuration; 256 return new ConfigChangeResult(); 257 } 258}