package org.apereo.cas.authentication.principal.resolvers; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apereo.cas.authentication.AuthenticationHandler; import org.apereo.cas.authentication.Credential; import org.apereo.cas.authentication.PrincipalException; import org.apereo.cas.authentication.principal.DefaultPrincipalFactory; import org.apereo.cas.authentication.principal.NullPrincipal; 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.support.MergingPersonAttributeDaoImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Delegates to one or more principal resolves in series to resolve a principal. The input to first configured resolver * is the authenticated credential; for every subsequent resolver, the input is a {@link Credential} whose ID is the * resolved principal ID of the previous resolver. * <p> * A common use case for this component is resolving a temporary principal ID from an X.509 credential followed by * a search (e.g. LDAP, database) for the final principal based on the temporary ID. * * @author Marvin S. Addison * @since 4.0.0 */ public class ChainingPrincipalResolver implements PrincipalResolver { private static final Logger LOGGER = LoggerFactory.getLogger(ChainingPrincipalResolver.class); /** * Factory to create the principal type. **/ private PrincipalFactory principalFactory = new DefaultPrincipalFactory(); /** * The chain of delegate resolvers that are invoked in order. */ private List<PrincipalResolver> chain; /** * Sets the resolver chain. The resolvers other than the first one MUST be capable of performing resolution * on the basis of {@link Credential#getId()} alone; * {@link PersonDirectoryPrincipalResolver} notably meets that requirement. * * @param chain List of delegate resolvers that are invoked in a chain. */ public void setChain(final List<PrincipalResolver> chain) { this.chain = chain; } public void setChain(final PrincipalResolver... chain) { this.chain = Arrays.stream(chain).collect(Collectors.toList()); } /** * {@inheritDoc} * Resolves a credential by delegating to each of the configured resolvers in sequence. Note that the * final principal is taken from the first resolved principal in the chain, yet attributes are merged. * * @param credential Authenticated credential. * @param principal Authenticated principal, if any. * @return The principal from the last configured resolver in the chain. */ @Override public Principal resolve(final Credential credential, final Principal principal, final AuthenticationHandler handler) { final List<Principal> principals = new ArrayList<>(); chain.stream() .filter(resolver -> resolver.supports(credential)) .forEach(resolver -> { LOGGER.debug("Invoking principal resolver [{}]", resolver); final Principal p = resolver.resolve(credential, principal, handler); if (p != null) { principals.add(p); } }); if (principals.isEmpty()) { LOGGER.warn("None of the principal resolvers in the chain were able to produce a principal"); return NullPrincipal.getInstance(); } final Map<String, Object> attributes = new HashMap<>(); principals.forEach(p -> { if (p != null) { LOGGER.debug("Resolved principal [{}]", p); if (p.getAttributes() != null && !p.getAttributes().isEmpty()) { LOGGER.debug("Adding attributes [{}] for the final principal", p.getAttributes()); attributes.putAll(p.getAttributes()); } } }); final long count = principals.stream() .map(p -> p.getId().trim().toLowerCase()) .distinct() .collect(Collectors.toSet()).size(); if (count > 1) { throw new PrincipalException("Resolved principals by the chain are not unique because principal resolvers have produced CAS principals " + "with different identifiers which typically is the result of a configuration issue.", Collections.emptyMap(), Collections.emptyMap()); } final String principalId = principal != null ? principal.getId() : principals.iterator().next().getId(); final Principal finalPrincipal = this.principalFactory.createPrincipal(principalId, attributes); LOGGER.debug("Final principal constructed by the chain of resolvers is [{}]", finalPrincipal); return finalPrincipal; } /** * Determines whether the credential is supported by this component by delegating to the first configured * resolver in the chain. * * @param credential The credential to check for support. * @return True if the first configured resolver in the chain supports the credential, false otherwise. */ @Override public boolean supports(final Credential credential) { return this.chain.get(0).supports(credential); } @Override public String toString() { return new ToStringBuilder(this) .append("chain", chain) .toString(); } @Override public IPersonAttributeDao getAttributeRepository() { final MergingPersonAttributeDaoImpl dao = new MergingPersonAttributeDaoImpl(); dao.setPersonAttributeDaos(this.chain.stream().map(PrincipalResolver::getAttributeRepository).collect(Collectors.toList())); return dao; } }