package org.apereo.cas.adaptors.x509.authentication.principal;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.cryptacular.x509.dn.Attribute;
import org.cryptacular.x509.dn.AttributeType;
import org.cryptacular.x509.dn.NameReader;
import org.cryptacular.x509.dn.RDN;
import org.cryptacular.x509.dn.RDNSequence;
import org.cryptacular.x509.dn.StandardAttributeType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Credential to principal resolver that extracts one or more attribute values
* from the certificate subject DN and combines them with intervening delimiters.
*
* @author Marvin S. Addison
* @since 3.4.4
*/
public class X509SubjectPrincipalResolver extends AbstractX509PrincipalResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(X509SubjectPrincipalResolver.class);
/**
* Pattern used to extract attribute names from descriptor.
*/
private static final Pattern ATTR_PATTERN = Pattern.compile("\\$(\\w+)");
/**
* Descriptor representing an abstract format of the principal to be resolved.
*/
private final String descriptor;
/**
* Sets the descriptor that describes for format of the principal ID to
* create from X.509 subject DN attributes. The descriptor is made up of
* common X.509 attribute names prefixed by "$", which are replaced by
* attribute values extracted from DN attribute values.
* <p>
* EXAMPLE:
* </p>
* {@code
* {@code
* <bean class="X509SubjectPrincipalResolver"
* p:descriptor="$UID@$DC.$DC" />
* }
* }
* <p>
* The above bean when applied to a certificate with the DN
* <p>
* <b>DC=edu, DC=vt/UID=jacky, CN=Jascarnella Ellagwonto</b></p>
* <p>
* produces the principal <strong>jacky@vt.edu</strong>.</p>
*
* @param descriptor Descriptor string where attribute names are prefixed with "$"
* to identify replacement by real attribute values from the subject DN.
* Valid attributes include common X.509 DN attributes such as the following:
* <ul>
* <li>C</li>
* <li>CN</li>
* <li>DC</li>
* <li>EMAILADDRESS</li>
* <li>L</li>
* <li>O</li>
* <li>OU</li>
* <li>SERIALNUMBER</li>
* <li>ST</li>
* <li>UID</li>
* <li>UNIQUEIDENTIFIER</li>
* </ul>
* For a complete list of supported attributes, see
* {@link org.cryptacular.x509.dn.StandardAttributeType}.
*/
public X509SubjectPrincipalResolver(final String descriptor) {
this.descriptor = descriptor;
}
/**
* Replaces placeholders in the descriptor with values extracted from attribute
* values in relative distinguished name components of the DN.
*
* @param certificate X.509 certificate credential.
* @return Resolved principal ID.
* @see AbstractX509PrincipalResolver#resolvePrincipalInternal(java.security.cert.X509Certificate)
*/
@Override
protected String resolvePrincipalInternal(final X509Certificate certificate) {
LOGGER.debug("Resolving principal for [{}]", certificate);
final StringBuffer sb = new StringBuffer();
final Matcher m = ATTR_PATTERN.matcher(this.descriptor);
final Map<String, AttributeContext> attrMap = new HashMap<>();
final RDNSequence rdnSequence = new NameReader(certificate).readSubject();
String name;
String[] values;
AttributeContext context;
while (m.find()) {
name = m.group(1);
if (!attrMap.containsKey(name)) {
values = getAttributeValues(rdnSequence,
StandardAttributeType.fromName(name));
attrMap.put(name, new AttributeContext(values));
}
context = attrMap.get(name);
m.appendReplacement(sb, context.nextValue());
}
m.appendTail(sb);
return sb.toString();
}
/**
* Gets the values of the given attribute contained in the DN.
* <p>
* <p><strong>NOTE:</strong> no escaping is done on special characters in the
* values, which could be different from what would appear in the string
* representation of the DN.</p>
*
* @param rdnSequence list of relative distinguished names
* that contains the attributes comprising the DN.
* @param attribute Attribute whose values will be retrieved.
* @return The attribute values for the given attribute in the order they
* appear would appear in the string representation of the DN or an empty
* array if the given attribute does not exist.
*/
private static String[] getAttributeValues(final RDNSequence rdnSequence, final AttributeType attribute) {
// Iterates sequence in reverse order as specified in section 2.1 of RFC 2253
final List<String> values = new ArrayList<>();
for (final RDN rdn : rdnSequence.backward()) {
for (final Attribute attr : rdn.getAttributes()) {
if (attr.getType().equals(attribute)) {
values.add(attr.getValue());
}
}
}
return values.toArray(new String[values.size()]);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.appendSuper(super.toString())
.append("descriptor", descriptor)
.toString();
}
private static class AttributeContext {
private int currentIndex;
private final String[] values;
/**
* Instantiates a new attribute context.
*
* @param values the values
*/
AttributeContext(final String[] values) {
this.values = values;
}
/**
* Retrieve the next value, by incrementing the current index.
*
* @return the string
* @throws IllegalStateException if no values are remaining.
*/
public String nextValue() {
if (this.currentIndex == this.values.length) {
throw new IllegalStateException("No values remaining for attribute");
}
return this.values[this.currentIndex++];
}
}
}