package org.apereo.cas.services; import groovy.lang.Binding; import groovy.lang.GroovyShell; import org.apache.commons.io.FileUtils; 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.util.RegexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Return a collection of allowed attributes for the principal, but additionally, * offers the ability to rename attributes on a per-service level. * * @author Misagh Moayyed * @since 4.1.0 */ public class ReturnMappedAttributeReleasePolicy extends AbstractRegisteredServiceAttributeReleasePolicy { private static final long serialVersionUID = -6249488544306639050L; private static final Logger LOGGER = LoggerFactory.getLogger(ReturnMappedAttributeReleasePolicy.class); private static final Pattern INLINE_GROOVY_PATTERN = RegexUtils.createPattern("groovy\\s*\\{(.+)\\}"); private static final Pattern FILE_GROOVY_PATTERN = RegexUtils.createPattern("file:(.+\\.groovy)"); private Map<String, String> allowedAttributes; /** * Instantiates a new Return mapped attribute release policy. */ public ReturnMappedAttributeReleasePolicy() { this(new TreeMap<>()); } /** * Instantiates a new Return mapped attribute release policy. * * @param allowedAttributes the allowed attributes */ public ReturnMappedAttributeReleasePolicy(final Map<String, String> allowedAttributes) { this.allowedAttributes = allowedAttributes; } /** * Sets the allowed attributes. * * @param allowed the allowed attributes. */ public void setAllowedAttributes(final Map<String, String> allowed) { this.allowedAttributes = allowed; } /** * Gets the allowed attributes. * * @return the allowed attributes */ public Map<String, String> getAllowedAttributes() { return new TreeMap<>(this.allowedAttributes); } @Override protected Map<String, Object> getAttributesInternal(final Principal principal, final Map<String, Object> attrs, final RegisteredService service) { final Map<String, Object> resolvedAttributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); resolvedAttributes.putAll(attrs); final Map<String, Object> attributesToRelease = new HashMap<>(resolvedAttributes.size()); /* * Map each entry in the allowed list into an array first * by the original key, value and the original entry itself. * Then process the array to populate the map for allowed attributes */ this.allowedAttributes.entrySet().stream() .map(entry -> { final String key = entry.getKey(); LOGGER.debug("Attempting to map allowed attribute name [{}]", key); return new Object[]{key, resolvedAttributes.get(key), entry}; }) .forEach(entry -> { final String mappedAttributeName = ((Map.Entry<String, String>) entry[2]).getValue(); final Matcher matcherInline = INLINE_GROOVY_PATTERN.matcher(mappedAttributeName); final Matcher matcherFile = FILE_GROOVY_PATTERN.matcher(mappedAttributeName); if (matcherInline.find()) { processInlineGroovyAttribute(resolvedAttributes, attributesToRelease, matcherInline, entry); } else if (matcherFile.find()) { processFileBasedGroovyAttributes(resolvedAttributes, attributesToRelease, matcherFile, entry); } else { if (entry[1] != null) { LOGGER.debug("Found attribute [{}] in the list of allowed attributes, mapped to the name [{}]", entry[0], mappedAttributeName); attributesToRelease.put(mappedAttributeName, entry[1]); } else { LOGGER.warn("Could not find value for mapped attribute [{}] that is based off of [{}] in the allowed attributes list", mappedAttributeName, entry[0]); } } }); return attributesToRelease; } private static void processFileBasedGroovyAttributes(final Map<String, Object> resolvedAttributes, final Map<String, Object> attributesToRelease, final Matcher matcherFile, final Object[] entry) { try { LOGGER.debug("Found groovy script to execute for attribute mapping [{}]", entry[0]); final String script = FileUtils.readFileToString(new File(matcherFile.group(1)), StandardCharsets.UTF_8); final Object result = getGroovyAttributeValue(script, resolvedAttributes); if (result != null) { LOGGER.debug("Mapped attribute [{}] to [{}] from script", entry[0], result); attributesToRelease.put(entry[0].toString(), result); } else { LOGGER.warn("Groovy-scripted attribute returned no value for [{}]", entry[0]); } } catch (final IOException e) { LOGGER.error(e.getMessage(), e); } } private static void processInlineGroovyAttribute(final Map<String, Object> resolvedAttributes, final Map<String, Object> attributesToRelease, final Matcher matcherInline, final Object[] entry) { LOGGER.debug("Found inline groovy script to execute for attribute mapping [{}]", entry[0]); final Object result = getGroovyAttributeValue(matcherInline.group(1), resolvedAttributes); if (result != null) { LOGGER.debug("Mapped attribute [{}] to [{}] from script", entry[0], result); attributesToRelease.put(entry[0].toString(), result); } else { LOGGER.warn("Groovy-scripted attribute returned no value for [{}]", entry[0]); } } private static Object getGroovyAttributeValue(final String groovyScript, final Map<String, Object> resolvedAttributes) { try { final Binding binding = new Binding(); final GroovyShell shell = new GroovyShell(binding); binding.setVariable("attributes", resolvedAttributes); binding.setVariable("logger", LOGGER); LOGGER.debug("Executing groovy script [{}] with attributes binding of [{}]", StringUtils.abbreviate(groovyScript, groovyScript.length() / 2), resolvedAttributes); final Object res = shell.evaluate(groovyScript); return res; } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } return null; } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (obj.getClass() != getClass()) { return false; } final ReturnMappedAttributeReleasePolicy rhs = (ReturnMappedAttributeReleasePolicy) obj; return new EqualsBuilder() .appendSuper(super.equals(obj)) .append(this.allowedAttributes, rhs.allowedAttributes) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder() .appendSuper(super.hashCode()) .append(this.allowedAttributes) .toHashCode(); } @Override public String toString() { return new ToStringBuilder(this) .appendSuper(super.toString()) .append("allowedAttributes", this.allowedAttributes) .toString(); } }