package org.apereo.cas.authentication.principal.cache;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalAttributesRepository;
import org.apereo.cas.util.spring.ApplicationContextProvider;
import org.apereo.services.persondir.IPersonAttributeDao;
import org.apereo.services.persondir.IPersonAttributes;
import org.apereo.services.persondir.support.merger.IAttributeMerger;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import java.io.Closeable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Parent class for retrieval principals attributes, provides operations
* around caching, merging of attributes.
*
* @author Misagh Moayyed
* @since 4.2
*/
public abstract class AbstractPrincipalAttributesRepository implements PrincipalAttributesRepository, Closeable {
/**
* Default cache expiration time unit.
*/
private static final String DEFAULT_CACHE_EXPIRATION_UNIT = TimeUnit.HOURS.name();
/**
* Default expiration lifetime based on the default time unit.
*/
private static final long DEFAULT_CACHE_EXPIRATION_DURATION = 2;
private static final long serialVersionUID = 6350245643948535906L;
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractPrincipalAttributesRepository.class);
/**
* The expiration time.
*/
protected long expiration;
/**
* Expiration time unit.
*/
protected String timeUnit;
/**
* The merging strategy that deals with existing principal attributes
* and those that are retrieved from the source. By default, existing attributes
* are ignored and the source is always consulted.
*/
protected MergingStrategy mergingStrategy;
/**
* Defines the merging strategy options.
*/
public enum MergingStrategy {
/**
* Replace attributes.
*/
REPLACE,
/**
* Add attributes.
*/
ADD,
/**
* No merging.
*/
NONE,
/**
* Multivalued attributes.
*/
MULTIVALUED;
/**
* Get attribute merger.
*
* @return the attribute merger
*/
public IAttributeMerger getAttributeMerger() {
final String name = this.name().toUpperCase();
switch (name.toUpperCase()) {
case "REPLACE":
return new ReplacingAttributeAdder();
case "ADD":
return new NoncollidingAttributeAdder();
case "MULTIVALUED":
return new MultivaluedAttributeMerger();
default:
return null;
}
}
}
private transient IPersonAttributeDao attributeRepository;
/**
* Instantiates a new principal attributes repository.
* Simply used buy
*/
protected AbstractPrincipalAttributesRepository() {
this(DEFAULT_CACHE_EXPIRATION_DURATION, DEFAULT_CACHE_EXPIRATION_UNIT);
}
/**
* Instantiates a new principal attributes repository.
*
* @param expiration the expiration
* @param timeUnit the time unit
*/
public AbstractPrincipalAttributesRepository(final long expiration, final String timeUnit) {
this.expiration = expiration;
this.timeUnit = timeUnit;
}
/**
* The merging strategy that deals with existing principal attributes
* and those that are retrieved from the source. By default, existing attributes
* are ignored and the source is always consulted.
*
* @param mergingStrategy the strategy to use for conflicts
*/
public void setMergingStrategy(final MergingStrategy mergingStrategy) {
this.mergingStrategy = mergingStrategy;
}
public MergingStrategy getMergingStrategy() {
return this.mergingStrategy;
}
/**
* Convert person attributes to principal attributes.
*
* @param attributes person attributes
* @return principal attributes
*/
protected Map<String, Object> convertPersonAttributesToPrincipalAttributes(
final Map<String, List<Object>> attributes) {
return attributes.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> entry.getValue().size() == 1 ? entry.getValue().get(0) : entry.getValue(), (e, f) -> f == null ? e : f));
}
/***
* Convert principal attributes to person attributes.
* @param p the principal carrying attributes
* @return person attributes
*/
private static Map<String, List<Object>> convertPrincipalAttributesToPersonAttributes(final Principal p) {
final Map<String, List<Object>> convertedAttributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
final Map<String, Object> principalAttributes = p.getAttributes();
principalAttributes.entrySet().stream().forEach(entry -> {
final Object values = entry.getValue();
final String key = entry.getKey();
if (values instanceof List) {
convertedAttributes.put(key, (List) values);
} else {
convertedAttributes.put(key, Collections.singletonList(values));
}
});
return convertedAttributes;
}
/**
* Obtains attributes first from the repository by calling
* {@link org.apereo.services.persondir.IPersonAttributeDao#getPerson(String)}.
*
* @param id the person id to locate in the attribute repository
* @return the map of attributes
*/
protected Map<String, List<Object>> retrievePersonAttributesToPrincipalAttributes(final String id) {
final IPersonAttributes attrs = getAttributeRepository().getPerson(id);
if (attrs == null) {
LOGGER.debug("Could not find principal [{}] in the repository so no attributes are returned.", id);
return Collections.emptyMap();
}
final Map<String, List<Object>> attributes = attrs.getAttributes();
if (attributes == null) {
LOGGER.debug("Principal [{}] has no attributes and so none are returned.", id);
return Collections.emptyMap();
}
return attributes;
}
@Override
public Map<String, Object> getAttributes(final Principal p) {
final Map<String, Object> cachedAttributes = getPrincipalAttributes(p);
if (cachedAttributes != null && !cachedAttributes.isEmpty()) {
LOGGER.debug("Found [{}] cached attributes for principal [{}] that are [{}]", cachedAttributes.size(), p.getId(),
cachedAttributes);
return cachedAttributes;
}
if (getAttributeRepository() == null) {
LOGGER.debug("No attribute repository is defined for [{}]. Returning default principal attributes for [{}]",
getClass().getName(), p.getId());
return cachedAttributes;
}
final Map<String, List<Object>> sourceAttributes = retrievePersonAttributesToPrincipalAttributes(p.getId());
LOGGER.debug("Found [{}] attributes for principal [{}] from the attribute repository.",
sourceAttributes.size(), p.getId());
if (this.mergingStrategy == null || this.mergingStrategy.getAttributeMerger() == null) {
LOGGER.debug("No merging strategy found, so attributes retrieved from the repository will be used instead.");
return convertAttributesToPrincipalAttributesAndCache(p, sourceAttributes);
}
final Map<String, List<Object>> principalAttributes = convertPrincipalAttributesToPersonAttributes(p);
LOGGER.debug("Merging current principal attributes with that of the repository via strategy [{}]",
this.mergingStrategy.getClass().getSimpleName());
try {
final Map<String, List<Object>> mergedAttributes =
this.mergingStrategy.getAttributeMerger().mergeAttributes(principalAttributes, sourceAttributes);
return convertAttributesToPrincipalAttributesAndCache(p, mergedAttributes);
} catch (final Exception e) {
final StringBuilder builder = new StringBuilder();
builder.append(e.getClass().getName().concat("-"));
if (StringUtils.isNotBlank(e.getMessage())) {
builder.append(e.getMessage());
}
LOGGER.error("The merging strategy [{}] for [{}] has failed to produce principal attributes because: [{}]. "
+ "This usually is indicative of a bug and/or configuration mismatch. CAS will skip the merging process "
+ "and will return the original collection of principal attributes [{}]",
this.mergingStrategy,
p.getId(),
builder.toString(),
principalAttributes);
return convertAttributesToPrincipalAttributesAndCache(p, principalAttributes);
}
}
/**
* Convert attributes to principal attributes and cache.
*
* @param p the p
* @param sourceAttributes the source attributes
* @return the map
*/
private Map<String, Object> convertAttributesToPrincipalAttributesAndCache(final Principal p,
final Map<String, List<Object>> sourceAttributes) {
final Map<String, Object> finalAttributes = convertPersonAttributesToPrincipalAttributes(sourceAttributes);
addPrincipalAttributes(p.getId(), finalAttributes);
return finalAttributes;
}
/**
* Add principal attributes into the underlying cache instance.
*
* @param id identifier used by the cache as key.
* @param attributes attributes to cache
* @since 4.2
*/
protected abstract void addPrincipalAttributes(String id, Map<String, Object> attributes);
/**
* Gets principal attributes from cache.
*
* @param p the principal
* @return the principal attributes from cache
*/
protected abstract Map<String, Object> getPrincipalAttributes(Principal p);
public void setAttributeRepository(final IPersonAttributeDao attributeRepository) {
this.attributeRepository = attributeRepository;
}
private IPersonAttributeDao getAttributeRepository() {
if (this.attributeRepository == null) {
final ApplicationContext context = ApplicationContextProvider.getApplicationContext();
if (context != null) {
return context.getBean("attributeRepository", IPersonAttributeDao.class);
}
LOGGER.warn("No application context could be retrieved, so no attribute repository instance can be determined.");
}
return this.attributeRepository;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("mergingStrategy", this.mergingStrategy)
.append("expiration", this.expiration)
.append("timeUnit", this.timeUnit)
.toString();
}
public long getExpiration() {
return this.expiration;
}
public String getTimeUnit() {
return this.timeUnit;
}
public void setTimeUnit(final String unit) {
this.timeUnit = unit;
}
public void setExpiration(final long expiration) {
this.expiration = expiration;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
final AbstractPrincipalAttributesRepository rhs = (AbstractPrincipalAttributesRepository) obj;
return new EqualsBuilder()
.append(this.timeUnit, rhs.timeUnit)
.append(this.expiration, rhs.expiration)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(13, 133)
.append(this.timeUnit)
.append(this.expiration)
.toHashCode();
}
}