001/*
002 * (C) Copyright 2006-2007 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 * $Id: DefaultActionFilter.java 30476 2008-02-22 09:13:23Z bstefanescu $
020 */
021
022package org.nuxeo.ecm.platform.actions;
023
024import java.util.HashMap;
025import java.util.Map;
026
027import javax.el.ELException;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.common.xmap.annotation.XNode;
032import org.nuxeo.common.xmap.annotation.XNodeList;
033import org.nuxeo.common.xmap.annotation.XObject;
034import org.nuxeo.ecm.core.api.CoreSession;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.NuxeoPrincipal;
037
038/**
039 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
040 * @author <a href="mailto:rspivak@nuxeo.com">Ruslan Spivak</a>
041 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
042 */
043@XObject("filter")
044public class DefaultActionFilter implements ActionFilter, Cloneable {
045
046    private static final Log log = LogFactory.getLog(DefaultActionFilter.class);
047
048    @XNode("@id")
049    protected String id;
050
051    @XNode("@append")
052    protected boolean append;
053
054    @XNodeList(value = "rule", type = String[].class, componentType = FilterRule.class)
055    protected FilterRule[] rules;
056
057    public DefaultActionFilter() {
058        this(null, null, false);
059    }
060
061    public DefaultActionFilter(String id, FilterRule[] rules) {
062        this(id, rules, false);
063    }
064
065    public DefaultActionFilter(String id, FilterRule[] rules, boolean append) {
066        this.id = id;
067        this.rules = rules;
068        this.append = append;
069    }
070
071    @Override
072    public String getId() {
073        return id;
074    }
075
076    @Override
077    public void setId(String id) {
078        this.id = id;
079    }
080
081    public FilterRule[] getRules() {
082        return rules;
083    }
084
085    public void setRules(FilterRule[] rules) {
086        this.rules = rules;
087    }
088
089    // FIXME: the parameter 'action' is not used!
090    @Override
091    public boolean accept(Action action, ActionContext context) {
092        if (log.isDebugEnabled()) {
093            if (action == null) {
094                log.debug(String.format("#accept: checking filter '%s'", getId()));
095            } else {
096                log.debug(String.format("#accept: checking filter '%s' for action '%s'", getId(), action.getId()));
097            }
098        }
099        // no context: reject
100        if (context == null) {
101            if (log.isDebugEnabled()) {
102                log.debug("#accept: no context available: action filtered");
103            }
104            return false;
105        }
106        // no rule: accept
107        if (rules == null || rules.length == 0) {
108            return true;
109        }
110        boolean existsGrantRule = false;
111        boolean grantApply = false;
112        for (FilterRule rule : rules) {
113            boolean ruleApplies = checkRule(rule, context);
114            if (!rule.grant) {
115                if (ruleApplies) {
116                    if (log.isDebugEnabled()) {
117                        log.debug("#accept: denying rule applies => action filtered");
118                    }
119                    return false;
120                }
121            } else {
122                existsGrantRule = true;
123                if (ruleApplies) {
124                    grantApply = true;
125                }
126            }
127        }
128        if (existsGrantRule) {
129            if (log.isDebugEnabled()) {
130                if (grantApply) {
131                    log.debug("#accept: granting rule applies, action not filtered");
132                } else {
133                    log.debug("#accept: granting rule applies, action filtered");
134                }
135            }
136            return grantApply;
137        }
138        // there is no allow rule, and none of the deny rules applies
139        return true;
140    }
141
142    public static final String PRECOMPUTED_KEY = "PrecomputedFilters";
143
144    /**
145     * Returns true if all conditions defined in the rule are true.
146     * <p>
147     * Since 5.7.3, does not put computed value in context in a cache if the action context does not allow it.
148     *
149     * @see ActionContext#disableGlobalCaching()
150     */
151    @SuppressWarnings("unchecked")
152    protected final boolean checkRule(FilterRule rule, ActionContext context) {
153        if (log.isDebugEnabled()) {
154            log.debug(String.format("#checkRule: checking rule '%s'", rule));
155        }
156        boolean disableCache = context.disableGlobalCaching();
157        if (!disableCache) {
158            // check cache
159            Map<FilterRule, Boolean> precomputed = (Map<FilterRule, Boolean>) context.getLocalVariable(PRECOMPUTED_KEY);
160            if (precomputed != null && precomputed.containsKey(rule)) {
161                if (log.isDebugEnabled()) {
162                    log.debug(String.format("#checkRule: return precomputed result for rule '%s'", rule));
163                }
164                return Boolean.TRUE.equals(precomputed.get(rule));
165            }
166        }
167        // compute filter result
168        boolean result = (rule.facets == null || rule.facets.length == 0 || checkFacets(context, rule.facets))
169                && (rule.types == null || rule.types.length == 0 || checkTypes(context, rule.types))
170                && (rule.schemas == null || rule.schemas.length == 0 || checkSchemas(context, rule.schemas))
171                && (rule.permissions == null || rule.permissions.length == 0 || checkPermissions(context,
172                        rule.permissions))
173                && (rule.groups == null || rule.groups.length == 0 || checkGroups(context, rule.groups))
174                && (rule.conditions == null || rule.conditions.length == 0 || checkConditions(context, rule.conditions));
175        if (!disableCache) {
176            // put in cache
177            Map<FilterRule, Boolean> precomputed = (Map<FilterRule, Boolean>) context.getLocalVariable(PRECOMPUTED_KEY);
178            if (precomputed == null) {
179                precomputed = new HashMap<>();
180                context.putLocalVariable(PRECOMPUTED_KEY, precomputed);
181            }
182            precomputed.put(rule, Boolean.valueOf(result));
183        }
184        return result;
185    }
186
187    /**
188     * Returns true if document has one of the given facets, else false.
189     *
190     * @return true if document has one of the given facets, else false.
191     */
192    protected final boolean checkFacets(ActionContext context, String[] facets) {
193        DocumentModel doc = context.getCurrentDocument();
194        if (doc == null) {
195            return false;
196        }
197        for (String facet : facets) {
198            if (doc.hasFacet(facet)) {
199                if (log.isDebugEnabled()) {
200                    log.debug(String.format("#checkFacets: return true for facet '%s'", facet));
201                }
202                return true;
203            }
204        }
205        if (log.isDebugEnabled()) {
206            log.debug("#checkFacets: return false");
207        }
208        return false;
209    }
210
211    /**
212     * Returns true if given document has one of the permissions, else false.
213     * <p>
214     * If no document is found, return true only if principal is a manager.
215     *
216     * @return true if given document has one of the given permissions, else false
217     */
218    protected final boolean checkPermissions(ActionContext context, String[] permissions) {
219        DocumentModel doc = context.getCurrentDocument();
220        if (doc == null) {
221            NuxeoPrincipal principal = context.getCurrentPrincipal();
222            // default check when there is not context yet
223            if (principal != null) {
224                if (principal.isAdministrator()) {
225                    if (log.isDebugEnabled()) {
226                        log.debug("#checkPermissions: doc is null but user is admin => return true");
227                    }
228                    return true;
229                }
230            }
231            if (log.isDebugEnabled()) {
232                log.debug("#checkPermissions: doc and user are null => return false");
233            }
234            return false;
235        }
236        // check rights on doc
237        CoreSession docMgr = context.getDocumentManager();
238        if (docMgr == null) {
239            if (log.isDebugEnabled()) {
240                log.debug("#checkPermissions: no core session => return false");
241            }
242            return false;
243        }
244        for (String permission : permissions) {
245            if (docMgr.hasPermission(doc.getRef(), permission)) {
246                if (log.isDebugEnabled()) {
247                    log.debug(String.format("#checkPermissions: return true for permission '%s'", permission));
248                }
249                return true;
250            }
251        }
252        if (log.isDebugEnabled()) {
253            log.debug("#checkPermissions: return false");
254        }
255        return false;
256    }
257
258    protected final boolean checkGroups(ActionContext context, String[] groups) {
259        NuxeoPrincipal principal = context.getCurrentPrincipal();
260        if (principal == null) {
261            if (log.isDebugEnabled()) {
262                log.debug("#checkGroups: no user => return false");
263            }
264            return false;
265        }
266        for (String group : groups) {
267            if (principal.isMemberOf(group)) {
268                if (log.isDebugEnabled()) {
269                    log.debug(String.format("#checkGroups: return true for group '%s'", group));
270                }
271                return true;
272            }
273        }
274        if (log.isDebugEnabled()) {
275            log.debug("#checkGroups: return false");
276        }
277        return false;
278    }
279
280    /**
281     * Returns true if one of the conditions is verified, else false.
282     * <p>
283     * If one evaluation fails, return false.
284     *
285     * @return true if one of the conditions is verified, else false.
286     */
287    protected final boolean checkConditions(ActionContext context, String[] conditions) {
288        for (String condition : conditions) {
289            try {
290                if (context.checkCondition(condition)) {
291                    if (log.isDebugEnabled()) {
292                        log.debug(String.format("#checkCondition: return true for condition '%s'", condition));
293                    }
294                    return true;
295                }
296            } catch (ELException e) {
297                log.error("evaluation of condition " + condition + " failed: returning false", e);
298                return false;
299            }
300        }
301        if (log.isDebugEnabled()) {
302            log.debug("#checkConditions: return false");
303        }
304        return false;
305    }
306
307    /**
308     * Returns true if document type is one of the given types, else false.
309     * <p>
310     * If document is null, consider context is the server and return true if 'Server' is in the list.
311     *
312     * @return true if document type is one of the given types, else false.
313     */
314    protected final boolean checkTypes(ActionContext context, String[] types) {
315        DocumentModel doc = context.getCurrentDocument();
316        String docType;
317        if (doc == null) {
318            // consider we're on the Server root
319            docType = "Root";
320        } else {
321            docType = doc.getType();
322        }
323
324        for (String type : types) {
325            if (type.equals(docType)) {
326                if (log.isDebugEnabled()) {
327                    log.debug(String.format("#checkTypes: return true for type '%s'", docType));
328                }
329                return true;
330            }
331        }
332        if (log.isDebugEnabled()) {
333            log.debug("#checkTypes: return false");
334        }
335        return false;
336    }
337
338    /**
339     * Returns true if document has one of the given schemas, else false.
340     *
341     * @return true if document has one of the given schemas, else false
342     */
343    protected final boolean checkSchemas(ActionContext context, String[] schemas) {
344        DocumentModel doc = context.getCurrentDocument();
345        if (doc == null) {
346            if (log.isDebugEnabled()) {
347                log.debug("#checkSchemas: no doc => return false");
348            }
349            return false;
350        }
351        for (String schema : schemas) {
352            if (doc.hasSchema(schema)) {
353                if (log.isDebugEnabled()) {
354                    log.debug(String.format("#checkSchemas: return true for schema '%s'", schema));
355                }
356                return true;
357            }
358        }
359        if (log.isDebugEnabled()) {
360            log.debug("#checkSchemas: return false");
361        }
362        return false;
363    }
364
365    public boolean getAppend() {
366        return append;
367    }
368
369    public void setAppend(boolean append) {
370        this.append = append;
371    }
372
373    @Override
374    public DefaultActionFilter clone() {
375        DefaultActionFilter clone = new DefaultActionFilter();
376        clone.id = id;
377        clone.append = append;
378        if (rules != null) {
379            clone.rules = new FilterRule[rules.length];
380            for (int i = 0; i < rules.length; i++) {
381                clone.rules[i] = rules[i].clone();
382            }
383        }
384        return clone;
385    }
386
387    /**
388     * Equals method added to handle hot reload of inner filters, see NXP-9677
389     *
390     * @since 5.6
391     */
392    @Override
393    public boolean equals(Object obj) {
394        if (obj == null) {
395            return false;
396        }
397        if (this == obj) {
398            return true;
399        }
400        if (!(obj instanceof DefaultActionFilter)) {
401            return false;
402        }
403        final DefaultActionFilter o = (DefaultActionFilter) obj;
404        String objId = o.getId();
405        if (objId == null && !(this.id == null)) {
406            return false;
407        }
408        if (this.id == null && !(objId == null)) {
409            return false;
410        }
411        if (objId != null && !objId.equals(this.id)) {
412            return false;
413        }
414        boolean append = o.getAppend();
415        if (!append == this.append) {
416            return false;
417        }
418        FilterRule[] objRules = o.getRules();
419        if (objRules == null && !(this.rules == null)) {
420            return false;
421        }
422        if (this.rules == null && !(objRules == null)) {
423            return false;
424        }
425        if (objRules != null) {
426            if (objRules.length != this.rules.length) {
427                return false;
428            }
429            for (int i = 0; i < objRules.length; i++) {
430                if (objRules[i] == null && (!(this.rules[i] == null))) {
431                    return false;
432                }
433                if (this.rules[i] == null && (!(objRules[i] == null))) {
434                    return false;
435                }
436                if (!objRules[i].equals(this.rules[i])) {
437                    return false;
438                }
439            }
440        }
441        return true;
442    }
443}