package org.apereo.cas.config;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.principal.resolvers.InternalGroovyScriptDao;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.model.core.authentication.PrincipalAttributesProperties;
import org.apereo.cas.configuration.support.Beans;
import org.apereo.services.persondir.IPersonAttributeDao;
import org.apereo.services.persondir.support.CachingPersonAttributeDaoImpl;
import org.apereo.services.persondir.support.GroovyPersonAttributeDao;
import org.apereo.services.persondir.support.GrouperPersonAttributeDao;
import org.apereo.services.persondir.support.JsonBackedComplexStubPersonAttributeDao;
import org.apereo.services.persondir.support.MergingPersonAttributeDaoImpl;
import org.apereo.services.persondir.support.jdbc.AbstractJdbcPersonAttributeDao;
import org.apereo.services.persondir.support.jdbc.MultiRowJdbcPersonAttributeDao;
import org.apereo.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao;
import org.apereo.services.persondir.support.ldap.LdaptivePersonAttributeDao;
import org.apereo.services.persondir.support.merger.MultivaluedAttributeMerger;
import org.apereo.services.persondir.support.merger.NoncollidingAttributeAdder;
import org.apereo.services.persondir.support.merger.ReplacingAttributeAdder;
import org.jooq.lambda.Unchecked;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.OrderComparator;
import org.springframework.core.io.Resource;
import javax.naming.directory.SearchControls;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* This is {@link CasPersonDirectoryConfiguration}.
*
* @author Misagh Moayyed
* @since 5.0.0
*/
@Configuration("casPersonDirectoryConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CasPersonDirectoryConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(CasPersonDirectoryConfiguration.class);
@Autowired
private ApplicationContext applicationContext;
@Autowired
private CasConfigurationProperties casProperties;
@ConditionalOnMissingBean(name = "attributeRepositories")
@Bean
@RefreshScope
public List<IPersonAttributeDao> attributeRepositories() {
final List<IPersonAttributeDao> list = new ArrayList<>();
list.addAll(ldapAttributeRepositories());
list.addAll(jdbcAttributeRepositories());
list.addAll(jsonAttributeRepositories());
list.addAll(groovyAttributeRepositories());
list.addAll(grouperAttributeRepositories());
list.addAll(stubAttributeRepositories());
OrderComparator.sort(list);
LOGGER.debug("Final list of attribute repositories is [{}]", list);
return list;
}
@ConditionalOnMissingBean(name = "attributeRepository")
@Bean
@RefreshScope
public IPersonAttributeDao attributeRepository() {
return composeMergedAndCachedAttributeRepositories(attributeRepositories());
}
@ConditionalOnMissingBean(name = "jsonAttributeRepositories")
@Bean
@RefreshScope
public List<IPersonAttributeDao> jsonAttributeRepositories() {
final List<IPersonAttributeDao> list = new ArrayList<>();
casProperties.getAuthn().getAttributeRepository().getJson().forEach(Unchecked.consumer(json -> {
final Resource r = json.getConfig().getLocation();
if (r != null) {
final JsonBackedComplexStubPersonAttributeDao dao = new JsonBackedComplexStubPersonAttributeDao(r);
dao.setOrder(json.getOrder());
dao.init();
LOGGER.debug("Configured JSON attribute sources from [{}]", r);
list.add(dao);
}
}));
return list;
}
@ConditionalOnMissingBean(name = "groovyAttributeRepositories")
@Bean
@RefreshScope
public List<IPersonAttributeDao> groovyAttributeRepositories() {
final List<IPersonAttributeDao> list = new ArrayList<>();
casProperties.getAuthn().getAttributeRepository().getGroovy().forEach(groovy -> {
if (groovy.getConfig().getLocation() != null) {
final GroovyPersonAttributeDao dao = new GroovyPersonAttributeDao(new InternalGroovyScriptDao(applicationContext, casProperties));
dao.setCaseInsensitiveUsername(groovy.isCaseInsensitive());
dao.setOrder(groovy.getOrder());
LOGGER.debug("Configured Groovy attribute sources from [{}]", groovy.getConfig().getLocation());
list.add(dao);
}
});
return list;
}
@ConditionalOnMissingBean(name = "grouperAttributeRepositories")
@Bean
@RefreshScope
public List<IPersonAttributeDao> grouperAttributeRepositories() {
final List<IPersonAttributeDao> list = new ArrayList<>();
final PrincipalAttributesProperties.Grouper gp = casProperties.getAuthn().getAttributeRepository().getGrouper();
if (gp.isEnabled()) {
final GrouperPersonAttributeDao dao = new GrouperPersonAttributeDao();
dao.setOrder(gp.getOrder());
LOGGER.debug("Configured Grouper attribute source");
list.add(dao);
}
return list;
}
@ConditionalOnMissingBean(name = "stubAttributeRepositories")
@Bean
@RefreshScope
public List<IPersonAttributeDao> stubAttributeRepositories() {
final List<IPersonAttributeDao> list = new ArrayList<>();
final Map<String, String> attrs = casProperties.getAuthn().getAttributeRepository().getStub().getAttributes();
if (!attrs.isEmpty() && list.isEmpty()) {
LOGGER.info("Found and added static attributes [{}] to the list of candidate attribute repositories", attrs.keySet());
list.add(Beans.newStubAttributeRepository(casProperties.getAuthn().getAttributeRepository()));
}
return list;
}
@ConditionalOnMissingBean(name = "jdbcAttributeRepositories")
@Bean
@RefreshScope
public List<IPersonAttributeDao> jdbcAttributeRepositories() {
final List<IPersonAttributeDao> list = new ArrayList<>();
final PrincipalAttributesProperties attrs = casProperties.getAuthn().getAttributeRepository();
attrs.getJdbc().forEach(jdbc -> {
if (StringUtils.isNotBlank(jdbc.getSql()) && StringUtils.isNotBlank(jdbc.getUrl())) {
final AbstractJdbcPersonAttributeDao jdbcDao;
if (jdbc.isSingleRow()) {
LOGGER.debug("Configured single-row JDBC attribute repository for [{}]", jdbc.getUrl());
jdbcDao = new SingleRowJdbcPersonAttributeDao(
Beans.newDataSource(jdbc),
jdbc.getSql()
);
} else {
LOGGER.debug("Configured multi-row JDBC attribute repository for [{}]", jdbc.getUrl());
jdbcDao = new MultiRowJdbcPersonAttributeDao(
Beans.newDataSource(jdbc),
jdbc.getSql()
);
LOGGER.debug("Configured multi-row JDBC column mappings for [{}] are [{}]", jdbc.getUrl(), jdbc.getColumnMappings());
((MultiRowJdbcPersonAttributeDao) jdbcDao).setNameValueColumnMappings(jdbc.getColumnMappings());
}
jdbcDao.setQueryAttributeMapping(Collections.singletonMap("username", jdbc.getUsername()));
final Map<String, String> mapping = jdbc.getAttributes();
if (mapping != null && !mapping.isEmpty()) {
LOGGER.debug("Configured result attribute mapping for [{}] to be [{}]", jdbc.getUrl(), jdbc.getAttributes());
jdbcDao.setResultAttributeMapping(mapping);
}
jdbcDao.setRequireAllQueryAttributes(jdbc.isRequireAllAttributes());
jdbcDao.setUsernameCaseCanonicalizationMode(jdbc.getCaseCanonicalization());
jdbcDao.setDefaultCaseCanonicalizationMode(jdbc.getCaseCanonicalization());
jdbcDao.setQueryType(jdbc.getQueryType());
jdbcDao.setOrder(jdbc.getOrder());
list.add(jdbcDao);
}
});
return list;
}
@ConditionalOnMissingBean(name = "ldapAttributeRepositories")
@Bean
@RefreshScope
public List<IPersonAttributeDao> ldapAttributeRepositories() {
final List<IPersonAttributeDao> list = new ArrayList<>();
final PrincipalAttributesProperties attrs = casProperties.getAuthn().getAttributeRepository();
attrs.getLdap().forEach(ldap -> {
if (StringUtils.isNotBlank(ldap.getBaseDn()) && StringUtils.isNotBlank(ldap.getLdapUrl())) {
final LdaptivePersonAttributeDao ldapDao = new LdaptivePersonAttributeDao();
LOGGER.debug("Configured LDAP attribute source for [{}] and baseDn [{}]", ldap.getLdapUrl(), ldap.getBaseDn());
ldapDao.setConnectionFactory(Beans.newLdaptivePooledConnectionFactory(ldap));
ldapDao.setBaseDN(ldap.getBaseDn());
LOGGER.debug("LDAP attributes are fetched from [{}] via filter [{}]", ldap.getLdapUrl(), ldap.getUserFilter());
ldapDao.setSearchFilter(ldap.getUserFilter());
final SearchControls constraints = new SearchControls();
if (ldap.getAttributes() != null && !ldap.getAttributes().isEmpty()) {
LOGGER.debug("Configured result attribute mapping for [{}] to be [{}]", ldap.getLdapUrl(), ldap.getAttributes());
ldapDao.setResultAttributeMapping(ldap.getAttributes());
final String[] attributes = ldap.getAttributes().keySet().toArray(new String[ldap.getAttributes().keySet().size()]);
constraints.setReturningAttributes(attributes);
} else {
LOGGER.debug("Retrieving all attributes as no explicit attribute mappings are defined for [{}]", ldap.getLdapUrl());
constraints.setReturningAttributes(null);
}
if (ldap.isSubtreeSearch()) {
LOGGER.debug("Configured subtree searching for [{}]", ldap.getLdapUrl());
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
constraints.setDerefLinkFlag(true);
ldapDao.setSearchControls(constraints);
ldapDao.setOrder(ldap.getOrder());
LOGGER.debug("Initializing LDAP attribute source for [{}]", ldap.getLdapUrl());
ldapDao.initialize();
list.add(ldapDao);
}
});
return list;
}
private IPersonAttributeDao composeMergedAndCachedAttributeRepositories(final List<IPersonAttributeDao> list) {
final MergingPersonAttributeDaoImpl mergingDao = new MergingPersonAttributeDaoImpl();
final String merger = StringUtils.defaultIfBlank(casProperties.getAuthn().getAttributeRepository().getMerger(), "replace".trim());
LOGGER.debug("Configured merging strategy for attribute sources is [{}]", merger);
switch (merger.toLowerCase()) {
case "merge":
mergingDao.setMerger(new MultivaluedAttributeMerger());
break;
case "add":
mergingDao.setMerger(new NoncollidingAttributeAdder());
break;
case "replace":
default:
mergingDao.setMerger(new ReplacingAttributeAdder());
break;
}
final CachingPersonAttributeDaoImpl impl = new CachingPersonAttributeDaoImpl();
impl.setCacheNullResults(false);
final Cache graphs = CacheBuilder.newBuilder()
.concurrencyLevel(2)
.weakKeys()
.maximumSize(casProperties.getAuthn().getAttributeRepository().getMaximumCacheSize())
.expireAfterWrite(casProperties.getAuthn().getAttributeRepository().getExpireInMinutes(), TimeUnit.MINUTES)
.build();
impl.setUserInfoCache(graphs.asMap());
mergingDao.setPersonAttributeDaos(list);
impl.setCachedPersonAttributesDao(mergingDao);
if (list.isEmpty()) {
LOGGER.debug("No attribute repository sources are available/defined to merge together.");
} else {
LOGGER.debug("Configured attribute repository sources to merge together: [{}]", list);
LOGGER.debug("Configured cache expiration policy for merging attribute sources to be [{}] minute(s)",
casProperties.getAuthn().getAttributeRepository().getExpireInMinutes());
}
return impl;
}
}