/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2010-2023 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.security.auth.x500.X500Principal; import com.unboundid.directory.sdk.common.types.InternalConnection; import com.unboundid.directory.sdk.ds.api.CertificateMapper; import com.unboundid.directory.sdk.ds.config.CertificateMapperConfig; import com.unboundid.directory.sdk.ds.types.DirectoryServerContext; import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.Filter; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPSearchException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.ldap.sdk.SearchRequest; import com.unboundid.ldap.sdk.SearchResultEntry; import com.unboundid.ldap.sdk.SearchScope; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.DNArgument; import com.unboundid.util.args.StringArgument; /** * This class provides a simple example of a certificate mapper which will * expect to find an attribute with the certificate's subject in the appropriate * user's entry. It has two configuration arguments: * <UL> * <LI>base-dn -- The base DN below which to search for user entries. At * least one base DN must be configured.</LI> * <LI>subject-attribute -- The name of the attribute in which to search for * the certificate subject.</LI> * </UL> */ public final class ExampleCertificateMapper extends CertificateMapper { /** * The name of the argument that will be used to specify the name of the * attribute to search for the certificate subject. */ private static final String ARG_NAME_BASE_DN = "base-dn"; /** * The name of the argument that will be used to specify the name of the * attribute to search for the certificate subject. */ private static final String ARG_NAME_SUBJECT_ATTR = "subject-attribute"; // The server context for the server in which this extension is running. private DirectoryServerContext serverContext; // The list of base DNs to use for the searches. private volatile List<DN> baseDNs; // The name of the subject attribute. private volatile String subjectAttribute; /** * Creates a new instance of this certificate mapper. All certificate mapper * implementations must include a default constructor, but any initialization * should generally be done in the {@code initializeCertificateMapper} method. */ public ExampleCertificateMapper() { // No implementation required. } /** * Retrieves a human-readable name for this extension. * * @return A human-readable name for this extension. */ @Override() public String getExtensionName() { return "Example Certificate Mapper"; } /** * Retrieves a human-readable description for this extension. Each element * of the array that is returned will be considered a separate paragraph in * generated documentation. * * @return A human-readable description for this extension, or {@code null} * or an empty array if no description should be available. */ @Override() public String[] getExtensionDescription() { return new String[] { "This certificate mapper serves an example that may be used to " + "demonstrate the process for creating a third-party certificate " + "mapper. In order to establish a mapping, exactly one entry must " + "contain a specified attribute whose value is the subject of the " + "certificate that has been presented. The mapping will fail if " + "no matching entries are found, or if multiple matches are found." }; } /** * Updates the provided argument parser to define any configuration arguments * which may be used by this certificate mapper. The argument parser may also * be updated to define relationships between arguments (e.g., to specify * required, exclusive, or dependent argument sets). * * @param parser The argument parser to be updated with the configuration * arguments which may be used by this certificate mapper. * * @throws ArgumentException If a problem is encountered while updating the * provided argument parser. */ @Override() public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { // Add an argument that allows you to specify the base DN for users. Character shortIdentifier = null; String longIdentifier = ARG_NAME_BASE_DN; boolean required = true; int maxOccurrences = 0; // No maximum. String placeholder = "{dn}"; String description = "The base DN below which to search for user " + "entries."; parser.addArgument(new DNArgument(shortIdentifier, longIdentifier, required, maxOccurrences, placeholder, description)); // Add an argument that allows you to specify the subject attribute. shortIdentifier = null; longIdentifier = ARG_NAME_SUBJECT_ATTR; required = true; maxOccurrences = 1; placeholder = "{attribute}"; description = "The name of the attribute in user entries which holds " + "the subject of the certificate(s) that user may use to " + "authenticate. It should be indexed for equality below all " + "configured base DNs."; parser.addArgument(new StringArgument(shortIdentifier, longIdentifier, required, maxOccurrences, placeholder, description)); } /** * Initializes this certificate mapper. * * @param serverContext A handle to the server context for the server in * which this extension is running. * @param config The general configuration for this certificate * mapper. * @param parser The argument parser which has been initialized from * the configuration for this certificate mapper. * * @throws LDAPException If a problem occurs while initializing this * certificate mapper. */ @Override() public void initializeCertificateMapper( final DirectoryServerContext serverContext, final CertificateMapperConfig config, final ArgumentParser parser) throws LDAPException { serverContext.debugInfo("Beginning certificate mapper initialization"); this.serverContext = serverContext; // Get the set of base DNs. final DNArgument dnArg = (DNArgument) parser.getNamedArgument(ARG_NAME_BASE_DN); baseDNs = dnArg.getValues(); // Get the subject attribute. final StringArgument attrArg = (StringArgument) parser.getNamedArgument(ARG_NAME_SUBJECT_ATTR); subjectAttribute = attrArg.getValue(); } /** * Indicates whether the configuration contained in the provided argument * parser represents a valid configuration for this extension. * * @param config The general configuration for this certificate * mapper. * @param parser The argument parser which has been initialized * with the proposed configuration. * @param unacceptableReasons A list that can be updated with reasons that * the proposed configuration is not acceptable. * * @return {@code true} if the proposed configuration is acceptable, or * {@code false} if not. */ @Override() public boolean isConfigurationAcceptable(final CertificateMapperConfig config, final ArgumentParser parser, final List<String> unacceptableReasons) { // No special validation is required. return true; } /** * Attempts to apply the configuration contained in the provided argument * parser. * * @param config The general configuration for this * certificate mapper. * @param parser The argument parser which has been * initialized with the new configuration. * @param adminActionsRequired A list that can be updated with information * about any administrative actions that may be * required before one or more of the * configuration changes will be applied. * @param messages A list that can be updated with information * about the result of applying the new * configuration. * * @return A result code that provides information about the result of * attempting to apply the configuration change. */ @Override() public ResultCode applyConfiguration(final CertificateMapperConfig config, final ArgumentParser parser, final List<String> adminActionsRequired, final List<String> messages) { // Get the new set of base DNs. final DNArgument dnArg = (DNArgument) parser.getNamedArgument(ARG_NAME_BASE_DN); final List<DN> newBaseDNs = dnArg.getValues(); // Get the new subject attribute. final StringArgument attrArg = (StringArgument) parser.getNamedArgument(ARG_NAME_SUBJECT_ATTR); final String newSubjectAttr = attrArg.getValue(); // Activate the new configuration. baseDNs = newBaseDNs; subjectAttribute = newSubjectAttr; return ResultCode.SUCCESS; } /** * Performs any cleanup which may be necessary when this certificate mapper is * to be taken out of service. */ @Override() public void finalizeCertificateMapper() { // No finalization is required. } /** * Performs any processing which may be necessary to map the provided * certificate chain to a user within the server. * * @param certChain The certificate chain presented by the client. * * @return The DN of the user within the server to which the provided * certificate corresponds. * * @throws LDAPException If the presented certificate cannot be mapped to * exactly one user in the server. */ @Override() public String mapCertificate(final Certificate[] certChain) throws LDAPException { final boolean debugEnabled = serverContext.debugEnabled(); // If the chain is empty, then we can't map it. This should never happen, // but it's not bad to handle it anyway. if ((certChain == null) || (certChain.length == 0)) { if (debugEnabled) { serverContext.debugInfo("No certificate provided."); } return null; } // Get the subject of the certificate. This is only supported for X.509 // certificates. if (! (certChain[0] instanceof X509Certificate)) { if (debugEnabled) { serverContext.debugInfo( "The provided certificate wasn't an X.509 certificate"); } return null; } final X509Certificate c = (X509Certificate) certChain[0]; final X500Principal p = c.getSubjectX500Principal(); final String subject = p.getName(X500Principal.RFC2253); if (debugEnabled) { serverContext.debugInfo("Certificate subject: " + subject); } // Construct the search request to issue. We'll swap out the base DN later. final Filter f = Filter.createEqualityFilter(subjectAttribute, subject); final SearchRequest r = new SearchRequest("", SearchScope.SUB, f, SearchRequest.NO_ATTRIBUTES); // Get an internal connection and use it to perform the searches. String userDN = null; final InternalConnection conn = serverContext.getInternalRootConnection(); for (final DN baseDN : baseDNs) { r.setBaseDN(baseDN); try { // Use the searchForEntry method, which will fail if multiple entries // are returned. However, we still need to check to see if a match was // already found under a different base DN. final SearchResultEntry searchEntry = conn.searchForEntry(r); if (searchEntry == null) { if (debugEnabled) { serverContext.debugInfo("No match was found below base DN " + baseDN); } continue; } else { if (debugEnabled) { serverContext.debugInfo("Found matching entry " + userDN); } } if (userDN == null) { userDN = searchEntry. getDN(); } else { throw new LDAPException(ResultCode.SIZE_LIMIT_EXCEEDED, "Multiple user entries were found with certificate subject " + subject); } } catch (final LDAPSearchException lse) { serverContext.debugCaught(lse); // We should examine the result code to determine how to proceed. switch (lse.getResultCode().intValue()) { case ResultCode.NO_SUCH_OBJECT_INT_VALUE: // This means that the base entry doesn't exist. We can ignore // this. break; case ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE: // This means that multiple entries were found matching the filter. throw new LDAPException(ResultCode.SIZE_LIMIT_EXCEEDED, "Multiple user entries were found with certificate subject " + subject, lse); default: // We'll just re-throw the exception as-is. throw lse; } } } // Return the DN of the matching user, or null if none was found. return userDN; } /** * Retrieves a map containing examples of configurations that may be used for * this extension. The map key should be a list of sample arguments, and the * corresponding value should be a description of the behavior that will be * exhibited by the extension when used with that configuration. * * @return A map containing examples of configurations that may be used for * this extension. It may be {@code null} or empty if there should * not be any example argument sets. */ @Override() public Map<List<String>,String> getExamplesArgumentSets() { final LinkedHashMap<List<String>,String> exampleMap = new LinkedHashMap<List<String>,String>(1); exampleMap.put( Arrays.asList( ARG_NAME_BASE_DN + "=dc=example,dc=com", ARG_NAME_SUBJECT_ATTR + "=myCertSubject"), "Attempt to map certificates to user entries by searching below " + "dc=example,dc=com for user entries containing a myCertSubject " + "attribute with a value equal to the subject of the presented " + "certificate."); return exampleMap; } }