package org.apereo.cas.config;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.LdapAuthenticationHandler;
import org.apereo.cas.authentication.principal.DefaultPrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import org.apereo.cas.authentication.principal.resolvers.ChainingPrincipalResolver;
import org.apereo.cas.authentication.principal.resolvers.EchoingPrincipalResolver;
import org.apereo.cas.authentication.support.DefaultAccountStateHandler;
import org.apereo.cas.authentication.support.LdapPasswordPolicyConfiguration;
import org.apereo.cas.authentication.support.OptionalWarningAccountStateHandler;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.model.support.ldap.LdapAuthenticationProperties;
import org.apereo.cas.configuration.support.Beans;
import org.apereo.cas.services.ServicesManager;
import org.apereo.services.persondir.IPersonAttributeDao;
import org.ldaptive.auth.AuthenticationResponseHandler;
import org.ldaptive.auth.Authenticator;
import org.ldaptive.auth.ext.ActiveDirectoryAuthenticationResponseHandler;
import org.ldaptive.auth.ext.EDirectoryAuthenticationResponseHandler;
import org.ldaptive.auth.ext.FreeIPAAuthenticationResponseHandler;
import org.ldaptive.auth.ext.PasswordExpirationAuthenticationResponseHandler;
import org.ldaptive.auth.ext.PasswordPolicyAuthenticationResponseHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Period;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
/**
* This is {@link LdapAuthenticationConfiguration} that attempts to create
* relevant authentication handlers for LDAP.
*
* @author Misagh Moayyed
* @since 5.0.0
*/
@Configuration("ldapAuthenticationConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class LdapAuthenticationConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(LdapAuthenticationConfiguration.class);
@Autowired
private CasConfigurationProperties casProperties;
@Autowired
@Qualifier("personDirectoryPrincipalResolver")
private PrincipalResolver personDirectoryPrincipalResolver;
@Autowired
@Qualifier("attributeRepositories")
private List<IPersonAttributeDao> attributeRepositories;
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
@ConditionalOnMissingBean(name = "ldapPrincipalFactory")
@Bean
public PrincipalFactory ldapPrincipalFactory() {
return new DefaultPrincipalFactory();
}
@Bean
public Collection<AuthenticationHandler> ldapAuthenticationHandlers() {
final Collection<AuthenticationHandler> handlers = new HashSet<>();
casProperties.getAuthn().getLdap()
.stream()
.filter(ldapInstanceConfigurationPredicate())
.forEach(l -> {
final Map<String, String> attributes = Beans.transformPrincipalAttributesListIntoMap(l.getPrincipalAttributeList());
LOGGER.debug("Created and mapped principal attributes [{}] for [{}]...", attributes, l.getLdapUrl());
LOGGER.debug("Creating ldap authenticator for [{}] and baseDn [{}]", l.getLdapUrl(), l.getBaseDn());
final Authenticator authenticator = Beans.newLdaptiveAuthenticator(l);
LOGGER.debug("Ldap authenticator configured with return attributes [{}] for [{}] and baseDn [{}]",
attributes.keySet(), l.getLdapUrl(), l.getBaseDn());
LOGGER.debug("Creating ldap authentication handler for [{}]", l.getLdapUrl());
final LdapAuthenticationHandler handler = new LdapAuthenticationHandler(l.getName(),
servicesManager, ldapPrincipalFactory(),
l.getOrder(), authenticator);
final List<String> additionalAttributes = l.getAdditionalAttributes();
if (StringUtils.isNotBlank(l.getPrincipalAttributeId())) {
additionalAttributes.add(l.getPrincipalAttributeId());
}
handler.setAllowMultiplePrincipalAttributeValues(l.isAllowMultiplePrincipalAttributeValues());
handler.setAllowMissingPrincipalAttributeValue(l.isAllowMissingPrincipalAttributeValue());
handler.setPasswordEncoder(Beans.newPasswordEncoder(l.getPasswordEncoder()));
handler.setPrincipalNameTransformer(Beans.newPrincipalNameTransformer(l.getPrincipalTransformation()));
if (StringUtils.isNotBlank(l.getCredentialCriteria())) {
LOGGER.debug("Ldap authentication for [{}] is filtering credentials by [{}]",
l.getLdapUrl(), l.getCredentialCriteria());
handler.setCredentialSelectionPredicate(Beans.newCredentialSelectionPredicate(l.getCredentialCriteria()));
}
if (StringUtils.isBlank(l.getPrincipalAttributeId())) {
LOGGER.debug("No principal id attribute is found for ldap authentication via [{}]", l.getLdapUrl());
} else {
handler.setPrincipalIdAttribute(l.getPrincipalAttributeId());
LOGGER.debug("Using principal id attribute [{}] for ldap authentication via [{}]", l.getPrincipalAttributeId(),
l.getLdapUrl());
}
if (l.getPasswordPolicy().isEnabled()) {
LOGGER.debug("Password policy is enabled for [{}]. Constructing password policy configuration", l.getLdapUrl());
handler.setPasswordPolicyConfiguration(createLdapPasswordPolicyConfiguration(l, authenticator, attributes));
}
handler.setPrincipalAttributeMap(attributes);
LOGGER.debug("Initializing ldap authentication handler for [{}]", l.getLdapUrl());
handler.initialize();
handlers.add(handler);
});
return handlers;
}
private static Predicate<LdapAuthenticationProperties> ldapInstanceConfigurationPredicate() {
return l -> {
if (l.getType() == null) {
LOGGER.warn("Skipping LDAP authentication entry since no type is defined");
return false;
}
if (StringUtils.isBlank(l.getLdapUrl())) {
LOGGER.warn("Skipping LDAP authentication entry since no ldap url is defined");
return false;
}
return true;
};
}
private static LdapPasswordPolicyConfiguration createLdapPasswordPolicyConfiguration(final LdapAuthenticationProperties l,
final Authenticator authenticator,
final Map<String, String> attributes) {
final LdapPasswordPolicyConfiguration cfg = new LdapPasswordPolicyConfiguration(l.getPasswordPolicy());
final Set<AuthenticationResponseHandler> handlers = new HashSet<>();
if (cfg.getPasswordWarningNumberOfDays() > 0) {
LOGGER.debug("Password policy authentication response handler is set to accommodate directory type: [{}]",
l.getPasswordPolicy().getType());
switch (l.getPasswordPolicy().getType()) {
case AD:
handlers.add(new ActiveDirectoryAuthenticationResponseHandler(Period.ofDays(cfg.getPasswordWarningNumberOfDays())));
Arrays.stream(ActiveDirectoryAuthenticationResponseHandler.ATTRIBUTES).forEach(a -> {
LOGGER.debug("Configuring authentication to retrieve password policy attribute [{}]", a);
attributes.put(a, a);
});
break;
case FreeIPA:
Arrays.stream(FreeIPAAuthenticationResponseHandler.ATTRIBUTES).forEach(a -> {
LOGGER.debug("Configuring authentication to retrieve password policy attribute [{}]", a);
attributes.put(a, a);
});
handlers.add(new FreeIPAAuthenticationResponseHandler(
Period.ofDays(cfg.getPasswordWarningNumberOfDays()), cfg.getLoginFailures()));
break;
case EDirectory:
Arrays.stream(EDirectoryAuthenticationResponseHandler.ATTRIBUTES).forEach(a -> {
LOGGER.debug("Configuring authentication to retrieve password policy attribute [{}]", a);
attributes.put(a, a);
});
handlers.add(new EDirectoryAuthenticationResponseHandler(Period.ofDays(cfg.getPasswordWarningNumberOfDays())));
break;
default:
handlers.add(new PasswordPolicyAuthenticationResponseHandler());
handlers.add(new PasswordExpirationAuthenticationResponseHandler());
break;
}
} else {
LOGGER.debug("Password warning number of days is undefined; LDAP authentication may NOT support "
+ "EDirectory, AD and FreeIPA to handle password policy authentication responses");
}
authenticator.setAuthenticationResponseHandlers((AuthenticationResponseHandler[]) handlers.toArray(
new AuthenticationResponseHandler[handlers.size()]));
LOGGER.debug("LDAP authentication response handlers configured are: [{}]", handlers);
if (StringUtils.isNotBlank(l.getPasswordPolicy().getWarningAttributeName())
&& StringUtils.isNotBlank(l.getPasswordPolicy().getWarningAttributeValue())) {
final OptionalWarningAccountStateHandler accountHandler = new OptionalWarningAccountStateHandler();
accountHandler.setDisplayWarningOnMatch(l.getPasswordPolicy().isDisplayWarningOnMatch());
accountHandler.setWarnAttributeName(l.getPasswordPolicy().getWarningAttributeName());
accountHandler.setWarningAttributeValue(l.getPasswordPolicy().getWarningAttributeValue());
accountHandler.setAttributesToErrorMap(l.getPasswordPolicy().getPolicyAttributes());
cfg.setAccountStateHandler(accountHandler);
LOGGER.debug("Configuring an warning account state handler for LDAP authentication for warning attribute [{}] and value [{}]",
l.getPasswordPolicy().getWarningAttributeName(), l.getPasswordPolicy().getWarningAttributeValue());
} else {
final DefaultAccountStateHandler accountHandler = new DefaultAccountStateHandler();
accountHandler.setAttributesToErrorMap(l.getPasswordPolicy().getPolicyAttributes());
cfg.setAccountStateHandler(accountHandler);
LOGGER.debug("Configuring the default account state handler for LDAP authentication");
}
return cfg;
}
/**
* The type Ldap authentication event execution plan configuration.
*/
@Configuration("ldapAuthenticationEventExecutionPlanConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class LdapAuthenticationEventExecutionPlanConfiguration implements AuthenticationEventExecutionPlanConfigurer {
private boolean isAttributeRepositorySourceDefined() {
return !attributeRepositories.isEmpty();
}
@Override
public void configureAuthenticationExecutionPlan(final AuthenticationEventExecutionPlan plan) {
ldapAuthenticationHandlers().forEach(handler -> {
final ChainingPrincipalResolver resolver = new ChainingPrincipalResolver();
if (isAttributeRepositorySourceDefined()) {
LOGGER.debug("Attribute repository sources are defined and available for the principal resolution chain");
resolver.setChain(Arrays.asList(personDirectoryPrincipalResolver, new EchoingPrincipalResolver()));
} else {
LOGGER.debug("Attribute repository sources are not available for principal resolution so principal resolver will echo "
+ "back the principal resolved during LDAP authentication directly.");
resolver.setChain(new EchoingPrincipalResolver());
}
LOGGER.info("Ldap authentication for [{}] is to chain principal resolvers via [{}] for attribute resolution",
handler.getName(), resolver);
plan.registerAuthenticationHandlerWithPrincipalResolver(handler, resolver);
});
}
}
}