/* * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Nuxeo - initial API and implementation * * $Id: DefaultActionFilter.java 30476 2008-02-22 09:13:23Z bstefanescu $ */ package org.nuxeo.ecm.platform.actions; import java.util.HashMap; import java.util.Map; import javax.el.ELException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.xmap.annotation.XNode; import org.nuxeo.common.xmap.annotation.XNodeList; import org.nuxeo.common.xmap.annotation.XObject; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.NuxeoPrincipal; /** * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> * @author <a href="mailto:rspivak@nuxeo.com">Ruslan Spivak</a> * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> */ @XObject("filter") public class DefaultActionFilter implements ActionFilter, Cloneable { private static final long serialVersionUID = 8885038533939001747L; private static final Log log = LogFactory.getLog(DefaultActionFilter.class); @XNode("@id") protected String id; @XNode("@append") protected boolean append; @XNodeList(value = "rule", type = String[].class, componentType = FilterRule.class) protected FilterRule[] rules; public DefaultActionFilter() { this(null, null, false); } public DefaultActionFilter(String id, FilterRule[] rules) { this(id, rules, false); } public DefaultActionFilter(String id, FilterRule[] rules, boolean append) { this.id = id; this.rules = rules; this.append = append; } public String getId() { return id; } public void setId(String id) { this.id = id; } public FilterRule[] getRules() { return rules; } public void setRules(FilterRule[] rules) { this.rules = rules; } // FIXME: the parameter 'action' is not used! public boolean accept(Action action, ActionContext context) { if (log.isDebugEnabled()) { if (action == null) { log.debug(String.format("#accept: checking filter '%s'", getId())); } else { log.debug(String.format("#accept: checking filter '%s' for action '%s'", getId(), action.getId())); } } // no context: reject if (context == null) { if (log.isDebugEnabled()) { log.debug("#accept: no context available: action filtered"); } return false; } // no rule: accept if (rules == null || rules.length == 0) { return true; } boolean existsGrantRule = false; boolean grantApply = false; for (FilterRule rule : rules) { boolean ruleApplies = checkRule(rule, context); if (!rule.grant) { if (ruleApplies) { if (log.isDebugEnabled()) { log.debug("#accept: denying rule applies => action filtered"); } return false; } } else { existsGrantRule = true; if (ruleApplies) { grantApply = true; } } } if (existsGrantRule) { if (log.isDebugEnabled()) { if (grantApply) { log.debug("#accept: granting rule applies, action not filtered"); } else { log.debug("#accept: granting rule applies, action filtered"); } } return grantApply; } // there is no allow rule, and none of the deny rules applies return true; } public static final String PRECOMPUTED_KEY = "PrecomputedFilters"; /** * Returns true if all conditions defined in the rule are true. * <p> * Since 5.7.3, does not put computed value in context in a cache if the action context does not allow it. * * @see ActionContext#disableGlobalCaching() */ @SuppressWarnings("unchecked") protected final boolean checkRule(FilterRule rule, ActionContext context) { if (log.isDebugEnabled()) { log.debug(String.format("#checkRule: checking rule '%s'", rule)); } boolean disableCache = context.disableGlobalCaching(); if (!disableCache) { // check cache Map<FilterRule, Boolean> precomputed = (Map<FilterRule, Boolean>) context.getLocalVariable(PRECOMPUTED_KEY); if (precomputed != null && precomputed.containsKey(rule)) { if (log.isDebugEnabled()) { log.debug(String.format("#checkRule: return precomputed result for rule '%s'", rule)); } return Boolean.TRUE.equals(precomputed.get(rule)); } } // compute filter result boolean result = (rule.facets == null || rule.facets.length == 0 || checkFacets(context, rule.facets)) && (rule.types == null || rule.types.length == 0 || checkTypes(context, rule.types)) && (rule.schemas == null || rule.schemas.length == 0 || checkSchemas(context, rule.schemas)) && (rule.permissions == null || rule.permissions.length == 0 || checkPermissions(context, rule.permissions)) && (rule.groups == null || rule.groups.length == 0 || checkGroups(context, rule.groups)) && (rule.conditions == null || rule.conditions.length == 0 || checkConditions(context, rule.conditions)); if (!disableCache) { // put in cache Map<FilterRule, Boolean> precomputed = (Map<FilterRule, Boolean>) context.getLocalVariable(PRECOMPUTED_KEY); if (precomputed == null) { precomputed = new HashMap<FilterRule, Boolean>(); context.putLocalVariable(PRECOMPUTED_KEY, precomputed); } precomputed.put(rule, Boolean.valueOf(result)); } return result; } /** * Returns true if document has one of the given facets, else false. * * @return true if document has one of the given facets, else false. */ protected final boolean checkFacets(ActionContext context, String[] facets) { DocumentModel doc = context.getCurrentDocument(); if (doc == null) { return false; } for (String facet : facets) { if (doc.hasFacet(facet)) { if (log.isDebugEnabled()) { log.debug(String.format("#checkFacets: return true for facet '%s'", facet)); } return true; } } if (log.isDebugEnabled()) { log.debug("#checkFacets: return false"); } return false; } /** * Returns true if given document has one of the permissions, else false. * <p> * If no document is found, return true only if principal is a manager. * * @return true if given document has one of the given permissions, else false */ protected final boolean checkPermissions(ActionContext context, String[] permissions) { DocumentModel doc = context.getCurrentDocument(); if (doc == null) { NuxeoPrincipal principal = context.getCurrentPrincipal(); // default check when there is not context yet if (principal != null) { if (principal.isAdministrator()) { if (log.isDebugEnabled()) { log.debug("#checkPermissions: doc is null but user is admin => return true"); } return true; } } if (log.isDebugEnabled()) { log.debug("#checkPermissions: doc and user are null => return false"); } return false; } // check rights on doc CoreSession docMgr = context.getDocumentManager(); if (docMgr == null) { if (log.isDebugEnabled()) { log.debug("#checkPermissions: no core session => return false"); } return false; } for (String permission : permissions) { if (docMgr.hasPermission(doc.getRef(), permission)) { if (log.isDebugEnabled()) { log.debug(String.format("#checkPermissions: return true for permission '%s'", permission)); } return true; } } if (log.isDebugEnabled()) { log.debug("#checkPermissions: return false"); } return false; } protected final boolean checkGroups(ActionContext context, String[] groups) { NuxeoPrincipal principal = context.getCurrentPrincipal(); if (principal == null) { if (log.isDebugEnabled()) { log.debug("#checkGroups: no user => return false"); } return false; } for (String group : groups) { if (principal.isMemberOf(group)) { if (log.isDebugEnabled()) { log.debug(String.format("#checkGroups: return true for group '%s'", group)); } return true; } } if (log.isDebugEnabled()) { log.debug("#checkGroups: return false"); } return false; } /** * Returns true if one of the conditions is verified, else false. * <p> * If one evaluation fails, return false. * * @return true if one of the conditions is verified, else false. */ protected final boolean checkConditions(ActionContext context, String[] conditions) { for (String condition : conditions) { try { if (context.checkCondition(condition)) { if (log.isDebugEnabled()) { log.debug(String.format("#checkCondition: return true for condition '%s'", condition)); } return true; } } catch (ELException e) { log.error("evaluation of condition " + condition + " failed: returning false", e); return false; } } if (log.isDebugEnabled()) { log.debug("#checkConditions: return false"); } return false; } /** * Returns true if document type is one of the given types, else false. * <p> * If document is null, consider context is the server and return true if 'Server' is in the list. * * @return true if document type is one of the given types, else false. */ protected final boolean checkTypes(ActionContext context, String[] types) { DocumentModel doc = context.getCurrentDocument(); String docType; if (doc == null) { // consider we're on the Server root docType = "Root"; } else { docType = doc.getType(); } for (String type : types) { if (type.equals(docType)) { if (log.isDebugEnabled()) { log.debug(String.format("#checkTypes: return true for type '%s'", docType)); } return true; } } if (log.isDebugEnabled()) { log.debug("#checkTypes: return false"); } return false; } /** * Returns true if document has one of the given schemas, else false. * * @return true if document has one of the given schemas, else false */ protected final boolean checkSchemas(ActionContext context, String[] schemas) { DocumentModel doc = context.getCurrentDocument(); if (doc == null) { if (log.isDebugEnabled()) { log.debug("#checkSchemas: no doc => return false"); } return false; } for (String schema : schemas) { if (doc.hasSchema(schema)) { if (log.isDebugEnabled()) { log.debug(String.format("#checkSchemas: return true for schema '%s'", schema)); } return true; } } if (log.isDebugEnabled()) { log.debug("#checkSchemas: return false"); } return false; } public boolean getAppend() { return append; } public void setAppend(boolean append) { this.append = append; } @Override public DefaultActionFilter clone() { DefaultActionFilter clone = new DefaultActionFilter(); clone.id = id; clone.append = append; if (rules != null) { clone.rules = new FilterRule[rules.length]; for (int i = 0; i < rules.length; i++) { clone.rules[i] = rules[i].clone(); } } return clone; } /** * Equals method added to handle hot reload of inner filters, see NXP-9677 * * @since 5.6 */ @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (!(obj instanceof DefaultActionFilter)) { return false; } final DefaultActionFilter o = (DefaultActionFilter) obj; String objId = o.getId(); if (objId == null && !(this.id == null)) { return false; } if (this.id == null && !(objId == null)) { return false; } if (objId != null && !objId.equals(this.id)) { return false; } boolean append = o.getAppend(); if (!append == this.append) { return false; } FilterRule[] objRules = o.getRules(); if (objRules == null && !(this.rules == null)) { return false; } if (this.rules == null && !(objRules == null)) { return false; } if (objRules != null) { if (objRules.length != this.rules.length) { return false; } for (int i = 0; i < objRules.length; i++) { if (objRules[i] == null && (!(this.rules[i] == null))) { return false; } if (this.rules[i] == null && (!(objRules[i] == null))) { return false; } if (!objRules[i].equals(this.rules[i])) { return false; } } } return true; } }