package org.apereo.cas.authentication.principal.resolvers;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.tuple.Pair;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.handler.PrincipalNameTransformer;
import org.apereo.cas.authentication.principal.DefaultPrincipalFactory;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import org.apereo.services.persondir.IPersonAttributeDao;
import org.apereo.services.persondir.IPersonAttributes;
import org.apereo.services.persondir.support.StubPersonAttributeDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Resolves principals by querying a data source using the Jasig
* <a href="http://developer.jasig.org/projects/person-directory/1.5.0-SNAPSHOT/apidocs/">Person Directory API</a>.
* The {@link Principal#getAttributes()} are populated by the results of the
* query and the principal ID may optionally be set by proving an attribute whose first non-null value is used;
* otherwise the credential ID is used for the principal ID.
*
* @author Marvin S. Addison
* @since 4.0.0
*/
public class PersonDirectoryPrincipalResolver implements PrincipalResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(PersonDirectoryPrincipalResolver.class);
/**
* Repository of principal attributes to be retrieved.
*/
protected IPersonAttributeDao attributeRepository = new StubPersonAttributeDao(new HashMap<>());
/**
* Factory to create the principal type.
**/
protected PrincipalFactory principalFactory = new DefaultPrincipalFactory();
/**
* return null if no attributes are found.
*/
protected boolean returnNullIfNoAttributes;
/**
* Transform principal name.
*/
protected PrincipalNameTransformer principalNameTransformer = formUserId -> formUserId;
/**
* Optional principal attribute name.
*/
protected String principalAttributeName;
public void setAttributeRepository(final IPersonAttributeDao attributeRepository) {
this.attributeRepository = attributeRepository;
}
public void setReturnNullIfNoAttributes(final boolean returnNullIfNoAttributes) {
this.returnNullIfNoAttributes = returnNullIfNoAttributes;
}
public void setPrincipalAttributeName(final String attribute) {
this.principalAttributeName = attribute;
}
public void setPrincipalFactory(final PrincipalFactory principalFactory) {
this.principalFactory = principalFactory;
}
@Override
public boolean supports(final Credential credential) {
return credential != null && credential.getId() != null;
}
@Override
public Principal resolve(final Credential credential, final Principal currentPrincipal, final AuthenticationHandler handler) {
LOGGER.debug("Attempting to resolve a principal...");
String principalId = extractPrincipalId(credential, currentPrincipal);
if (principalNameTransformer != null) {
principalId = principalNameTransformer.transform(principalId);
}
if (StringUtils.isBlank(principalId)) {
LOGGER.debug("Principal id [{}] could not be found", principalId);
return null;
}
LOGGER.debug("Creating principal for [{}]", principalId);
final Map<String, List<Object>> attributes = retrievePersonAttributes(principalId, credential);
if (attributes == null || attributes.isEmpty()) {
LOGGER.debug("Principal id [{}] did not specify any attributes", principalId);
if (!this.returnNullIfNoAttributes) {
LOGGER.debug("Returning the principal with id [{}] without any attributes", principalId);
return this.principalFactory.createPrincipal(principalId);
}
LOGGER.debug("[{}] is configured to return null if no attributes are found for [{}]",
this.getClass().getName(), principalId);
return null;
}
LOGGER.debug("Retrieved [{}] attribute(s) from the repository", attributes.size());
final Pair<String, Map<String, Object>> pair = convertPersonAttributesToPrincipal(principalId, attributes);
return this.principalFactory.createPrincipal(pair.getKey(), pair.getValue());
}
/**
* Convert person attributes to principal pair.
*
* @param extractedPrincipalId the extracted principal id
* @param attributes the attributes
* @return the pair
*/
protected Pair<String, Map<String, Object>> convertPersonAttributesToPrincipal(final String extractedPrincipalId,
final Map<String, List<Object>> attributes) {
final String[] principalId = {extractedPrincipalId};
final Map<String, Object> convertedAttributes = new HashMap<>();
attributes.entrySet().stream().forEach(entry -> {
final String key = entry.getKey();
LOGGER.debug("Found attribute [{}]", key);
final List<Object> values = entry.getValue();
if (StringUtils.isNotBlank(this.principalAttributeName) && key.equalsIgnoreCase(this.principalAttributeName)) {
if (values.isEmpty()) {
LOGGER.debug("[{}] is empty, using [{}] for principal", this.principalAttributeName, extractedPrincipalId);
} else {
principalId[0] = values.get(0).toString();
LOGGER.debug(
"Found principal attribute value [{}]; removing [{}] from attribute map.",
extractedPrincipalId,
this.principalAttributeName);
}
} else {
convertedAttributes.put(key, values.size() == 1 ? values.get(0) : values);
}
});
return Pair.of(principalId[0], convertedAttributes);
}
/**
* Retrieve person attributes map.
*
* @param principalId the principal id
* @param credential the credential whose id we have extracted. This is passed so that implementations
* can extract useful bits of authN info such as attributes into the principal.
* @return the map
*/
protected Map<String, List<Object>> retrievePersonAttributes(final String principalId, final Credential credential) {
final IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId);
final Map<String, List<Object>> attributes;
if (personAttributes == null) {
attributes = null;
} else {
attributes = personAttributes.getAttributes();
}
return attributes;
}
public void setPrincipalNameTransformer(final PrincipalNameTransformer principalNameTransformer) {
this.principalNameTransformer = principalNameTransformer;
}
/**
* Extracts the id of the user from the provided credential. This method should be overridden by subclasses to
* achieve more sophisticated strategies for producing a principal ID from a credential.
*
* @param credential the credential provided by the user.
* @param currentPrincipal the current principal
* @return the username, or null if it could not be resolved.
*/
protected String extractPrincipalId(final Credential credential, final Principal currentPrincipal) {
return credential.getId();
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("returnNullIfNoAttributes", returnNullIfNoAttributes)
.append("principalAttributeName", principalAttributeName)
.toString();
}
@Override
public IPersonAttributeDao getAttributeRepository() {
return this.attributeRepository;
}
}