/* * 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-2024 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples.groovy; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.List; import javax.security.auth.x500.X500Principal; import com.unboundid.directory.sdk.common.types.InternalConnection; import com.unboundid.directory.sdk.ds.config.CertificateMapperConfig; import com.unboundid.directory.sdk.ds.scripting.ScriptedCertificateMapper; 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 scripted 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 ExampleScriptedCertificateMapper extends ScriptedCertificateMapper { /** * 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 ExampleScriptedCertificateMapper() { // No implementation required. } /** * 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; } }