package org.apereo.cas.services.support;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apereo.cas.services.RegisteredServiceAttributeFilter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* The regex filter that is responsible to make sure only attributes that match a certain regex pattern
* registered service are released.
*
* @author Misagh Moayyed
* @since 4.0.0
*/
public class RegisteredServiceRegexAttributeFilter implements RegisteredServiceAttributeFilter {
private static final long serialVersionUID = 403015306984610128L;
private static final Logger LOGGER = LoggerFactory.getLogger(RegisteredServiceRegexAttributeFilter.class);
private Pattern pattern;
private int order;
/**
* Instantiates a new Registered service regex attribute filter.
* Required for serialization.
*/
protected RegisteredServiceRegexAttributeFilter() {
}
/**
* Instantiates a new registered service regex attribute filter.
*
* @param regex the regex
*/
public RegisteredServiceRegexAttributeFilter(final String regex) {
this.pattern = Pattern.compile(regex);
}
/**
* Gets the pattern.
*
* @return the pattern
*/
public Pattern getPattern() {
return this.pattern;
}
/**
* {@inheritDoc}
* <p>
* Given attribute values may be an extension of {@link Collection}, {@link Map} or an array.
* <ul>
* <li>The filtering operation is non-recursive. </li>
* <li>Multi-valued attributes such as those of type {@link Collection} and
* {@link Map} are expected to allow casting to {@code Map<String, String>}
* or {@code Collection<String>}.
* Values that are of type array are expected to allow casting to {@code String[]}.
* </li>
* <li>Multi-valued attributes are always put back into the final released collection of
* attributes as {@code String[]}.</li>
* <li>If the final filtered collection is empty, it will not be put into the collection of attributes.</li>
* </ul>
*/
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> filter(final Map<String, Object> givenAttributes) {
final Map<String, Object> attributesToRelease = new HashMap<>();
givenAttributes.entrySet()
.stream()
.filter(entry -> {
final String attributeName = entry.getKey();
final Object attributeValue = entry.getValue();
LOGGER.debug("Received attribute [{}] with value [{}]", attributeName, attributeValue);
return attributeValue != null;
})
.forEach(entry -> {
final String attributeName = entry.getKey();
final Object attributeValue = entry.getValue();
if (attributeValue instanceof Collection) {
LOGGER.trace("Attribute value [{}] is a collection", attributeValue);
final List filteredAttributes = filterAttributes((Collection<String>) attributeValue, attributeName);
if (!filteredAttributes.isEmpty()) {
attributesToRelease.put(attributeName, filteredAttributes);
}
} else if (attributeValue.getClass().isArray()) {
LOGGER.trace("Attribute value [{}] is an array", attributeValue);
final List filteredAttributes = filterAttributes(Arrays.asList((String[]) attributeValue), attributeName);
if (!filteredAttributes.isEmpty()) {
attributesToRelease.put(attributeName, filteredAttributes);
}
} else if (attributeValue instanceof Map) {
LOGGER.trace("Attribute value [{}] is a map", attributeValue);
final Map<String, String> filteredAttributes = filterAttributes((Map<String, String>) attributeValue);
if (!filteredAttributes.isEmpty()) {
attributesToRelease.put(attributeName, filteredAttributes);
}
} else {
LOGGER.trace("Attribute value [{}] is a string", attributeValue);
final String attrValue = attributeValue.toString();
if (patternMatchesAttributeValue(attrValue)) {
logReleasedAttributeEntry(attributeName, attrValue);
attributesToRelease.put(attributeName, attrValue);
}
}
});
LOGGER.debug("Received [{}] attributes. Filtered and released [{}]", givenAttributes.size(),
attributesToRelease.size());
return attributesToRelease;
}
@Override
public int getOrder() {
return order;
}
public void setOrder(final int order) {
this.order = order;
}
/**
* Filter map attributes based on the values given.
*
* @param valuesToFilter the values to filter
* @return the map
*/
private Map<String, String> filterAttributes(final Map<String, String> valuesToFilter) {
return valuesToFilter.entrySet()
.stream()
.filter(entry -> patternMatchesAttributeValue(entry.getValue())).map(entry -> {
logReleasedAttributeEntry(entry.getKey(), entry.getValue());
return entry;
})
.collect(Collectors.toMap(Map.Entry::getKey, entry -> valuesToFilter.get(entry.getKey()), (e, f) -> f == null ? e : f));
}
/**
* Determine whether pattern matches attribute value.
*
* @param value the value
* @return true, if successful
*/
private boolean patternMatchesAttributeValue(final String value) {
return this.pattern.matcher(value).matches();
}
/**
* Filter array attributes.
*
* @param valuesToFilter the values to filter
* @param attributeName the attribute name
* @return the string[]
*/
private List filterAttributes(final Collection<String> valuesToFilter, final String attributeName) {
return valuesToFilter.stream().filter(this::patternMatchesAttributeValue).map(attributeValue -> {
logReleasedAttributeEntry(attributeName, attributeValue);
return attributeValue;
}).collect(Collectors.toList());
}
/**
* Logs the released attribute entry.
*
* @param attributeName the attribute name
* @param attributeValue the attribute value
*/
private void logReleasedAttributeEntry(final String attributeName, final String attributeValue) {
LOGGER.debug("The attribute value [{}] for attribute name [{}] matches the pattern [{}]. Releasing attribute...",
attributeValue, attributeName, this.pattern.pattern());
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 83).append(this.pattern).toHashCode();
}
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
final RegisteredServiceRegexAttributeFilter rhs = (RegisteredServiceRegexAttributeFilter) obj;
return new EqualsBuilder().append(this.pattern.pattern(), rhs.getPattern().pattern()).isEquals();
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("pattern", this.pattern.pattern())
.toString();
}
}