it.geosolutions.geoserver.sira.security.expression.ExpressionRuleEngine.java Source code

Java tutorial

Introduction

Here is the source code for it.geosolutions.geoserver.sira.security.expression.ExpressionRuleEngine.java

Source

/*
 *  CSI SIRA - Access Manager Security Module ("Rules Engine"), a GeoServer Secure Catalog Resource Access Manager plugin with which specify advanced rules evaluated to decide what the specified user can access.
 *  Copyright (C) 2016  Regione Piemonte (www.regione.piemonte.it)
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package it.geosolutions.geoserver.sira.security.expression;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.geotools.filter.text.ecql.ECQL.toFilter;
import static org.geotools.util.logging.Logging.getLogger;
import static org.springframework.util.ReflectionUtils.findMethod;
import it.geosolutions.geoserver.sira.security.config.Attributes.Choose.When;
import it.geosolutions.geoserver.sira.security.config.Rule;
import it.geosolutions.geoserver.sira.security.config.SiraAccessManagerConfiguration;
import it.geosolutions.geoserver.sira.security.config.ValidatableConfiguration;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.geoserver.security.AccessMode;
import org.geoserver.security.impl.GeoServerUser;
import org.geoserver.security.iride.entity.IrideInfoPersona;
import org.geoserver.security.iride.util.IrideUserProperties;
import org.geotools.filter.text.cql2.CQLException;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.PropertyName;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.core.context.SecurityContextHolder;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

/**
 * <code>CSI</code> <code>SIRA</code> <code>Access Manager</code>
 * <a href="http://docs.spring.io/spring/docs/3.1.4.RELEASE/spring-framework-reference/html/expressions.html">Spring Expression Language (SpEL)</a> engine.
 *
 * @author "Simone Cornacchia - seancrow76@gmail.com, simone.cornacchia@consulenti.csi.it (CSI:71740)"
 */
public class ExpressionRuleEngine {

    /**
     * Logger.
     */
    private static final Logger LOGGER = getLogger(ExpressionRuleEngine.class);

    private final StandardEvaluationContext evalContext;

    private final ExpressionParser parser;

    /**
     * Constructor.
     */
    public ExpressionRuleEngine() {
        this.evalContext = new StandardEvaluationContext();
        this.parser = new SpelExpressionParser();
    }

    /**
     * Constructor.
     *
     * @param rootObject
     */
    public ExpressionRuleEngine(Object rootObject) {
        this();

        this.setRootObject(rootObject);
    }

    /**
     * Get the specialized {@link ParserContext}.
     *
     * @return the specialized {@link ParserContext}
     */
    public static ParserContext getTemplateExpression() {
        return ParserContext.TEMPLATE_EXPRESSION;
    }

    /**
     *
     * @param rootObject
     */
    public void setRootObject(Object rootObject) {
        this.evalContext.setRootObject(rootObject);
    }

    /**
     *
     * @param functions
     */
    public void setFunctions(Map<String, Method> functions) {
        checkNotNull(functions, "functions must not be null");

        for (final Entry<String, Method> entry : functions.entrySet()) {
            this.setFunction(entry.getKey(), entry.getValue());
        }
    }

    /**
     *
     * @param name
     * @param method
     */
    public void setFunction(String name, Method method) {
        this.evalContext.registerFunction(name, method);
    }

    /**
     *
     * @param rule
     * @return
     */
    public AccessMode evaluateAccessMode(Rule rule) {
        this.checkRulePreconditions(rule);

        final String result = this.evaluateExpression(rule.getAccessMode(), String.class);
        if (Rule.IGNORERULE.equals(result)) {
            return null;
        } else {
            try {
                return AccessMode.valueOf(result);
            } catch (IllegalArgumentException e) {
                LOGGER.log(Level.SEVERE, e.getMessage());
                throw new IllegalStateException(String.format(
                        "rule.accessMode expression must be evaluable as either READ, WRITE or IGNORERULE, but unknown '%s' was evaluated",
                        result), e);
            }
        }
    }

    /**
     * Evaluate the given {@link Rule#getFilter()} expression as a valid <code>ECQL</code> query predicate,
     * parsed and returned as an <code>ECQL</code> {@link Filter} instance, equivalent to the constraint specified in the predicate.
     *
     * @param rule the given {@link Rule} which expression is to be evaluated as a valid <code>ECQL</code> query predicate
     * @return the parsed <code>ECQL</code> {@link Filter} instance, equivalent to the constraint specified in the predicate
     * @throws CQLException if the evaluated given {@link Rule#getFilter()} expression is not a valid <code>ECQL</code> query predicate
     */
    public Filter evaluateFilter(Rule rule) throws CQLException {
        this.checkRulePreconditions(rule);

        return toECQLFilter(this.evaluateExpression(rule.getFilter(), String.class));
    }

    /**
     * Evaluate the given {@link When#getFilter()} expression as a valid <code>ECQL</code> query predicate,
     * parsed and returned as an <code>ECQL</code> {@link Filter} instance, equivalent to the constraint specified in the predicate.
     *
     * @param when the given {@link When} condition which filter expression is to be evaluated as a valid <code>ECQL</code> query predicate
     * @return the parsed <code>ECQL</code> {@link Filter} instance, equivalent to the constraint specified in the predicate
     * @throws CQLException if the evaluated given {@link When#getFilter()} expression is not a valid <code>ECQL</code> query predicate
     */
    public Filter evaluateFilter(When when) throws CQLException {
        this.checkRulePreconditions(when);

        return toECQLFilter(this.evaluateExpression(when.getFilter(), String.class));
    }

    /**
     *
     * @param rule
     * @return
     */
    public List<PropertyName> evaluateHiddenProperties(Rule rule) {
        this.checkRulePreconditions(rule);

        // consider attributes that are to be always hidden, if any...
        final List<PropertyName> hiddenProperties = Lists.newArrayList();
        for (final String defaultHiddenAttribute : rule.getHiddenAttributes().getDefaultAttributes()) {
            hiddenProperties.add(SiraAccessManagerConfiguration.FF.property(defaultHiddenAttribute));
        }

        return hiddenProperties;
    }

    /**
     *
     * @param filter
     * @return
     * @throws CQLException
     */
    private static Filter toECQLFilter(String filter) throws CQLException {
        return toFilter(filter, SiraAccessManagerConfiguration.FF);
    }

    /**
     * Check that the preconditions are met for the given {@link ValidatableConfiguration} instance.
     *
     * @param configuration the given {@link ValidatableConfiguration} instance
     * @throws NullPointerException if the given {@link ValidatableConfiguration} instance is {@code null}
     * @throws IllegalArgumentException if the given {@link ValidatableConfiguration} instance is not valid
     */
    private void checkRulePreconditions(ValidatableConfiguration configuration) {
        checkNotNull(configuration, "configuration must not be null");
        checkArgument(configuration.isValid(), "configuration must be valid, given: " + configuration);
    }

    /**
     *
     * @param expressionString
     * @param expectedResultType
     * @return
     */
    private <T> T evaluateExpression(String expressionString, Class<T> expectedResultType) {
        final Expression expression = this.parser.parseExpression(expressionString, getTemplateExpression());

        return expression.getValue(this.evalContext, expectedResultType);
    }

    /**
     * <code>CSI</code> <code>SIRA</code> <code>Access Manager</code>
     * <a href="http://docs.spring.io/spring/docs/3.1.4.RELEASE/spring-framework-reference/html/expressions.html">Spring Expression Language (SpEL)</a> engine
     * custom functions.
     *
     * @author "Simone Cornacchia - seancrow76@gmail.com, simone.cornacchia@consulenti.csi.it (CSI:71740)"
     */
    public static final class Functions {

        /**
         * Exported, immutable map of method name/{@link Method} instance pairs usable as {@link ExpressionRuleEngine#setFunctions(Map)} parameter, to register expression engine custom functions.
         * <p>Builtin custom functions:
         * <ul>
         *   <li>if : {@link #iif(boolean, Object, Object)}</li>
         *   <li>hasAuthority : {@link #hasAuthority(String, String)}</li>
         *   <li>hasIstatProvincia : {@link #hasIstatProvincia(String, String)}</li>
         *   <li>hasIstatComune : {@link #hasIstatComune(String, String)}</li>
         * <ul>
         */
        public static final Map<String, Method> BUILTINS = ImmutableMap.of("if",
                findMethod(Functions.class, "iif", new Class<?>[] { boolean.class, Object.class, Object.class }),
                "hasAuthority",
                findMethod(Functions.class, "hasAuthority", new Class<?>[] { String.class, String.class }),
                "hasIstatProvincia",
                findMethod(Functions.class, "hasIstatProvincia", new Class<?>[] { String.class, String.class }),
                "hasIstatComune",
                findMethod(Functions.class, "hasIstatComune", new Class<?>[] { String.class, String.class }));

        /**
         * Constructor.
         */
        private Functions() {
            /* NOP */
        }

        /**
         * A <code>Java</code> implementation of the <a href="https://msdn.microsoft.com/en-us/library/27ydhh0d(v=vs.90).aspx">Visual Basic <code>iif()</code> function</a>.
         *
         * <p>Returns one of two objects, <code>truthPart</code> or <code>falsePart</code>, depending on the evaluation of the given <code>expression</code>.
         *
         * <p>Similarly to <code>iif()</code> function (and contrary to <code>Java</code> native <code>ternary operator</code>),
         * <em>both</em> the returned and the unreturned values got evaluated, at call time,
         * while the <code>Java</code> native <code>ternary operator</code> short-circuits evaluating only the object actually returned.
         * <p>Therefore if there are side-effects to the evaluation, <code>iif()</code>
         * and <code>Java</code> native <code>ternary operator</code> are <em>not</em> equivalent.
         *
         * @param expression the given expression to evaluate
         * @param truePart object returned if <code>expression</code> evaluates to {@code true}
         * @param falsePart object returned if <code>expression</code> evaluates to {@code false}
         * @return one of two objects, depending on the evaluation of an <code>expression</code>
         */
        public static <T> T iif(boolean expression, T truePart, T falsePart) {
            return expression ? truePart : falsePart;
        }

        /**
         * Check to see if the given <code>ID_AUTORITA</code> {@code value} is present
         * in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         * returning {@code true} if found, {@code false} otherwise.
         *
         * @param role the given <code>IRIDE</code> {@code role}
         * @param value the given property {@code value}
         * @return {@code true} if the given <code>ID_AUTORITA</code> {@code value} is found
         *         in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         *         {@code false} otherwise.
         */
        public static boolean hasAuthority(String role, String value) {
            return hasInfoPersonaProperty(role, "ID_AUTORITA", value);
        }

        /**
         * Check to see if the given <code>ISTAT_PROVINCIA</code> {@code value} is present
         * in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         * returning {@code true} if found, {@code false} otherwise.
         *
         * @param role the given <code>IRIDE</code> {@code role}
         * @param value the given property {@code value}
         * @return {@code true} if the given <code>ISTAT_PROVINCIA</code> {@code value} is found
         *         in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         *         {@code false} otherwise.
         */
        public static boolean hasIstatProvincia(String role, String value) {
            return hasInfoPersonaProperty(role, "ISTAT_PROVINCIA", value);
        }

        /**
         * Check to see if the given <code>ISTAT_COMUNE</code> {@code value} is present
         * in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         * returning {@code true} if found, {@code false} otherwise.
         *
         * @param role the given <code>IRIDE</code> {@code role}
         * @param value the given property {@code value}
         * @return {@code true} if the given <code>ISTAT_COMUNE</code> {@code value} is found
         *         in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         *         {@code false} otherwise.
         */
        public static boolean hasIstatComune(String role, String value) {
            return hasInfoPersonaProperty(role, "ISTAT_COMUNE", value);
        }

        /**
         * Check to see if the given property ({@code key}) {@code value} is present
         * in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         * returning {@code true} if found, {@code false} otherwise.
         *
         * @param role the given <code>IRIDE</code> {@code role}
         * @param key the given property {@code key}
         * @param value the given property {@code value}
         * @return {@code true} if the given property ({@code key}) {@code value} is found
         *         in authenticated user's {@link IrideInfoPersona}s, for the given <code>IRIDE</code> {@code role},
         *         {@code false} otherwise.
         */
        private static boolean hasInfoPersonaProperty(String role, String key, String value) {
            if (isBlank(role) || isBlank(key) || isBlank(value)) {
                return false;
            }

            final Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            final Set<IrideInfoPersona> infoPersonae = getInfoPersonae((GeoServerUser) principal);
            for (final IrideInfoPersona ip : infoPersonae) {
                if (ip.getRole().getCode().equals(role)) {
                    final Map<String, Object> properties = ip.getProperties();
                    if (value.equals(properties.get(key))) {
                        return true;
                    }
                }
            }

            return false;
        }

        /**
         * Extracts the given {@link GeoServerUser} instance's {@link IrideInfoPersona}s.
         * If none are found, an empty, immutable set is returned.
         *
         * @param user the given {@link GeoServerUser} instance to extract {@link IrideInfoPersona}s from.
         * @return the given {@link GeoServerUser} instance's {@link IrideInfoPersona}s,
         *         or an empty, immutable set if none are found.
         */
        @SuppressWarnings("unchecked")
        private static Set<IrideInfoPersona> getInfoPersonae(GeoServerUser user) {
            final Properties properties = user.getProperties();
            if (properties.containsKey(IrideUserProperties.INFO_PERSONAE)) {
                return (Set<IrideInfoPersona>) properties.get(IrideUserProperties.INFO_PERSONAE);
            }

            return ImmutableSet.of();
        }

    }

}