/*
* 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 legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* 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 legal-notices/CDDLv1_0.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
*
*
* Copyright 2007-2008 Sun Microsystems, Inc.
* Portions Copyright 2012-2015 ForgeRock AS
* Portions Copyright 2013 Manuel Gaupp
*/
package org.opends.server.extensions;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.opends.server.util.CollectionUtils.*;
import static org.opends.server.util.StaticUtils.*;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.security.auth.x500.X500Principal;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.CertificateMapperCfg;
import org.opends.server.admin.std.server.SubjectAttributeToUserAttributeCertificateMapperCfg;
import org.opends.server.api.Backend;
import org.opends.server.api.CertificateMapper;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.IndexType;
import org.opends.server.types.InitializationException;
import org.opends.server.types.RDN;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
/**
* This class implements a very simple Directory Server certificate mapper that
* will map a certificate to a user based on attributes contained in both the
* certificate subject and the user's entry. The configuration may include
* mappings from certificate attributes to attributes in user entries, and all
* of those certificate attributes that are present in the subject will be used
* to search for matching user entries.
*/
public class SubjectAttributeToUserAttributeCertificateMapper
extends CertificateMapper<
SubjectAttributeToUserAttributeCertificateMapperCfg>
implements ConfigurationChangeListener<
SubjectAttributeToUserAttributeCertificateMapperCfg>
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** The DN of the configuration entry for this certificate mapper. */
private DN configEntryDN;
/** The mappings between certificate attribute names and user attribute types. */
private LinkedHashMap<String,AttributeType> attributeMap;
/** The current configuration for this certificate mapper. */
private SubjectAttributeToUserAttributeCertificateMapperCfg currentConfig;
/** The set of attributes to return in search result entries. */
private LinkedHashSet<String> requestedAttributes;
/**
* Creates a new instance of this certificate mapper. Note that all actual
* initialization should be done in the
* <CODE>initializeCertificateMapper</CODE> method.
*/
public SubjectAttributeToUserAttributeCertificateMapper()
{
super();
}
/** {@inheritDoc} */
@Override
public void initializeCertificateMapper(
SubjectAttributeToUserAttributeCertificateMapperCfg configuration)
throws ConfigException, InitializationException
{
configuration.addSubjectAttributeToUserAttributeChangeListener(this);
currentConfig = configuration;
configEntryDN = configuration.dn();
// Get and validate the subject attribute to user attribute mappings.
ConfigChangeResult ccr = new ConfigChangeResult();
attributeMap = buildAttributeMap(configuration, configEntryDN, ccr);
List<LocalizableMessage> messages = ccr.getMessages();
if (!messages.isEmpty())
{
throw new ConfigException(messages.iterator().next());
}
// Make sure that all the user attributes are configured with equality
// indexes in all appropriate backends.
Set<DN> cfgBaseDNs = getUserBaseDNs(configuration);
for (DN baseDN : cfgBaseDNs)
{
for (AttributeType t : attributeMap.values())
{
Backend<?> b = DirectoryServer.getBackend(baseDN);
if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
{
logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(),
t.getNameOrOID(), b.getBackendID());
}
}
}
// Create the attribute list to include in search requests. We want to
// include all user and operational attributes.
requestedAttributes = newLinkedHashSet("*", "+");
}
/** {@inheritDoc} */
@Override
public void finalizeCertificateMapper()
{
currentConfig.removeSubjectAttributeToUserAttributeChangeListener(this);
}
/** {@inheritDoc} */
@Override
public Entry mapCertificateToUser(Certificate[] certificateChain)
throws DirectoryException
{
SubjectAttributeToUserAttributeCertificateMapperCfg config = currentConfig;
LinkedHashMap<String,AttributeType> theAttributeMap = this.attributeMap;
// Make sure that a peer certificate was provided.
if (certificateChain == null || certificateChain.length == 0)
{
LocalizableMessage message = ERR_SATUACM_NO_PEER_CERTIFICATE.get();
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
}
// Get the first certificate in the chain. It must be an X.509 certificate.
X509Certificate peerCertificate;
try
{
peerCertificate = (X509Certificate) certificateChain[0];
}
catch (Exception e)
{
logger.traceException(e);
LocalizableMessage message = ERR_SATUACM_PEER_CERT_NOT_X509.get(certificateChain[0].getType());
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
}
// Get the subject from the peer certificate and use it to create a search
// filter.
DN peerDN;
X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
String peerName = peerPrincipal.getName(X500Principal.RFC2253);
try
{
peerDN = DN.valueOf(peerName);
}
catch (DirectoryException de)
{
LocalizableMessage message = ERR_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN.get(
peerName, de.getMessageObject());
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message, de);
}
LinkedList<SearchFilter> filterComps = new LinkedList<>();
for (int i=0; i < peerDN.size(); i++)
{
RDN rdn = peerDN.getRDN(i);
for (int j=0; j < rdn.getNumValues(); j++)
{
String lowerName = toLowerCase(rdn.getAttributeName(j));
// Try to normalize lowerName
lowerName = normalizeAttributeName(lowerName);
AttributeType attrType = theAttributeMap.get(lowerName);
if (attrType != null)
{
filterComps.add(SearchFilter.createEqualityFilter(attrType, rdn.getAttributeValue(j)));
}
}
}
if (filterComps.isEmpty())
{
LocalizableMessage message = ERR_SATUACM_NO_MAPPABLE_ATTRIBUTES.get(peerDN);
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
}
SearchFilter filter = SearchFilter.createANDFilter(filterComps);
Collection<DN> baseDNs = getUserBaseDNs(config);
// For each base DN, issue an internal search in an attempt to map the certificate.
Entry userEntry = null;
InternalClientConnection conn = getRootConnection();
for (DN baseDN : baseDNs)
{
final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
.setSizeLimit(1)
.setTimeLimit(10)
.addAttribute(requestedAttributes);
InternalSearchOperation searchOperation = conn.processSearch(request);
switch (searchOperation.getResultCode().asEnum())
{
case SUCCESS:
// This is fine. No action needed.
break;
case NO_SUCH_OBJECT:
// The search base doesn't exist. Not an ideal situation, but we'll
// ignore it.
break;
case SIZE_LIMIT_EXCEEDED:
// Multiple entries matched the filter. This is not acceptable.
LocalizableMessage message = ERR_SATUACM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(peerDN);
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
case TIME_LIMIT_EXCEEDED:
case ADMIN_LIMIT_EXCEEDED:
// The search criteria was too inefficient.
message = ERR_SATUACM_INEFFICIENT_SEARCH.get(peerDN, searchOperation.getErrorMessage());
throw new DirectoryException(searchOperation.getResultCode(), message);
default:
// Just pass on the failure that was returned for this search.
message = ERR_SATUACM_SEARCH_FAILED.get(peerDN, searchOperation.getErrorMessage());
throw new DirectoryException(searchOperation.getResultCode(), message);
}
for (SearchResultEntry entry : searchOperation.getSearchEntries())
{
if (userEntry == null)
{
userEntry = entry;
}
else
{
LocalizableMessage message = ERR_SATUACM_MULTIPLE_MATCHING_ENTRIES.
get(peerDN, userEntry.getName(), entry.getName());
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
}
}
}
// If we've gotten here, then we either found exactly one user entry or we
// didn't find any. Either way, return the entry or null to the caller.
return userEntry;
}
/** {@inheritDoc} */
@Override
public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
List<LocalizableMessage> unacceptableReasons)
{
SubjectAttributeToUserAttributeCertificateMapperCfg config =
(SubjectAttributeToUserAttributeCertificateMapperCfg) configuration;
return isConfigurationChangeAcceptable(config, unacceptableReasons);
}
/** {@inheritDoc} */
@Override
public boolean isConfigurationChangeAcceptable(
SubjectAttributeToUserAttributeCertificateMapperCfg configuration,
List<LocalizableMessage> unacceptableReasons)
{
ConfigChangeResult ccr = new ConfigChangeResult();
buildAttributeMap(configuration, configuration.dn(), ccr);
unacceptableReasons.addAll(ccr.getMessages());
return ResultCode.SUCCESS.equals(ccr.getResultCode());
}
/** {@inheritDoc} */
@Override
public ConfigChangeResult applyConfigurationChange(SubjectAttributeToUserAttributeCertificateMapperCfg configuration)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
LinkedHashMap<String, AttributeType> newAttributeMap = buildAttributeMap(configuration, configEntryDN, ccr);
// Make sure that all the user attributes are configured with equality
// indexes in all appropriate backends.
Set<DN> cfgBaseDNs = getUserBaseDNs(configuration);
for (DN baseDN : cfgBaseDNs)
{
for (AttributeType t : newAttributeMap.values())
{
Backend<?> b = DirectoryServer.getBackend(baseDN);
if (b != null && !b.isIndexed(t, IndexType.EQUALITY))
{
LocalizableMessage message =
WARN_SATUACM_ATTR_UNINDEXED.get(configuration.dn(), t.getNameOrOID(), b.getBackendID());
ccr.addMessage(message);
logger.error(message);
}
}
}
if (ccr.getResultCode() == ResultCode.SUCCESS)
{
attributeMap = newAttributeMap;
currentConfig = configuration;
}
return ccr;
}
/**
* If we have an explicit set of base DNs, then use it.
* Otherwise, use the set of public naming contexts in the server.
*/
private Set<DN> getUserBaseDNs(SubjectAttributeToUserAttributeCertificateMapperCfg config)
{
Set<DN> baseDNs = config.getUserBaseDN();
if (baseDNs == null || baseDNs.isEmpty())
{
baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
}
return baseDNs;
}
/** Get and validate the subject attribute to user attribute mappings. */
private LinkedHashMap<String, AttributeType> buildAttributeMap(
SubjectAttributeToUserAttributeCertificateMapperCfg configuration, DN cfgEntryDN, ConfigChangeResult ccr)
{
LinkedHashMap<String, AttributeType> results = new LinkedHashMap<>();
for (String mapStr : configuration.getSubjectAttributeMapping())
{
String lowerMap = toLowerCase(mapStr);
int colonPos = lowerMap.indexOf(':');
if (colonPos <= 0)
{
ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfgEntryDN, mapStr));
return null;
}
String certAttrName = lowerMap.substring(0, colonPos).trim();
String userAttrName = lowerMap.substring(colonPos+1).trim();
if (certAttrName.length() == 0 || userAttrName.length() == 0)
{
ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
ccr.addMessage(ERR_SATUACM_INVALID_MAP_FORMAT.get(cfgEntryDN, mapStr));
return null;
}
// Try to normalize the provided certAttrName
certAttrName = normalizeAttributeName(certAttrName);
if (results.containsKey(certAttrName))
{
ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
ccr.addMessage(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(cfgEntryDN, certAttrName));
return null;
}
AttributeType userAttrType = DirectoryServer.getAttributeTypeOrNull(userAttrName);
if (userAttrType == null)
{
ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
ccr.addMessage(ERR_SATUACM_NO_SUCH_ATTR.get(mapStr, cfgEntryDN, userAttrName));
return null;
}
if (results.values().contains(userAttrType))
{
ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
ccr.addMessage(ERR_SATUACM_DUPLICATE_USER_ATTR.get(cfgEntryDN, userAttrType.getNameOrOID()));
return null;
}
results.put(certAttrName, userAttrType);
}
return results;
}
/**
* Tries to normalize the given attribute name; if normalization is not
* possible the original String value is returned.
*
* @param attrName The attribute name which should be normalized.
* @return The normalized attribute name.
*/
private static String normalizeAttributeName(String attrName)
{
AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(attrName);
if (attrType != null)
{
String attrNameNormalized = attrType.getNormalizedPrimaryNameOrOID();
if (attrNameNormalized != null)
{
return attrNameNormalized;
}
}
return attrName;
}
}