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 long serialVersionUID = 8885038533939001747L;
047
048    private static final Log log = LogFactory.getLog(DefaultActionFilter.class);
049
050    @XNode("@id")
051    protected String id;
052
053    @XNode("@append")
054    protected boolean append;
055
056    @XNodeList(value = "rule", type = String[].class, componentType = FilterRule.class)
057    protected FilterRule[] rules;
058
059    public DefaultActionFilter() {
060        this(null, null, false);
061    }
062
063    public DefaultActionFilter(String id, FilterRule[] rules) {
064        this(id, rules, false);
065    }
066
067    public DefaultActionFilter(String id, FilterRule[] rules, boolean append) {
068        this.id = id;
069        this.rules = rules;
070        this.append = append;
071    }
072
073    public String getId() {
074        return id;
075    }
076
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    public boolean accept(Action action, ActionContext context) {
091        if (log.isDebugEnabled()) {
092            if (action == null) {
093                log.debug(String.format("#accept: checking filter '%s'", getId()));
094            } else {
095                log.debug(String.format("#accept: checking filter '%s' for action '%s'", getId(), action.getId()));
096            }
097        }
098        // no context: reject
099        if (context == null) {
100            if (log.isDebugEnabled()) {
101                log.debug("#accept: no context available: action filtered");
102            }
103            return false;
104        }
105        // no rule: accept
106        if (rules == null || rules.length == 0) {
107            return true;
108        }
109        boolean existsGrantRule = false;
110        boolean grantApply = false;
111        for (FilterRule rule : rules) {
112            boolean ruleApplies = checkRule(rule, context);
113            if (!rule.grant) {
114                if (ruleApplies) {
115                    if (log.isDebugEnabled()) {
116                        log.debug("#accept: denying rule applies => action filtered");
117                    }
118                    return false;
119                }
120            } else {
121                existsGrantRule = true;
122                if (ruleApplies) {
123                    grantApply = true;
124                }
125            }
126        }
127        if (existsGrantRule) {
128            if (log.isDebugEnabled()) {
129                if (grantApply) {
130                    log.debug("#accept: granting rule applies, action not filtered");
131                } else {
132                    log.debug("#accept: granting rule applies, action filtered");
133                }
134            }
135            return grantApply;
136        }
137        // there is no allow rule, and none of the deny rules applies
138        return true;
139    }
140
141    public static final String PRECOMPUTED_KEY = "PrecomputedFilters";
142
143    /**
144     * Returns true if all conditions defined in the rule are true.
145     * <p>
146     * Since 5.7.3, does not put computed value in context in a cache if the action context does not allow it.
147     *
148     * @see ActionContext#disableGlobalCaching()
149     */
150    @SuppressWarnings("unchecked")
151    protected final boolean checkRule(FilterRule rule, ActionContext context) {
152        if (log.isDebugEnabled()) {
153            log.debug(String.format("#checkRule: checking rule '%s'", rule));
154        }
155        boolean disableCache = context.disableGlobalCaching();
156        if (!disableCache) {
157            // check cache
158            Map<FilterRule, Boolean> precomputed = (Map<FilterRule, Boolean>) context.getLocalVariable(PRECOMPUTED_KEY);
159            if (precomputed != null && precomputed.containsKey(rule)) {
160                if (log.isDebugEnabled()) {
161                    log.debug(String.format("#checkRule: return precomputed result for rule '%s'", rule));
162                }
163                return Boolean.TRUE.equals(precomputed.get(rule));
164            }
165        }
166        // compute filter result
167        boolean result = (rule.facets == null || rule.facets.length == 0 || checkFacets(context, rule.facets))
168                && (rule.types == null || rule.types.length == 0 || checkTypes(context, rule.types))
169                && (rule.schemas == null || rule.schemas.length == 0 || checkSchemas(context, rule.schemas))
170                && (rule.permissions == null || rule.permissions.length == 0 || checkPermissions(context,
171                        rule.permissions))
172                && (rule.groups == null || rule.groups.length == 0 || checkGroups(context, rule.groups))
173                && (rule.conditions == null || rule.conditions.length == 0 || checkConditions(context, rule.conditions));
174        if (!disableCache) {
175            // put in cache
176            Map<FilterRule, Boolean> precomputed = (Map<FilterRule, Boolean>) context.getLocalVariable(PRECOMPUTED_KEY);
177            if (precomputed == null) {
178                precomputed = new HashMap<FilterRule, Boolean>();
179                context.putLocalVariable(PRECOMPUTED_KEY, precomputed);
180            }
181            precomputed.put(rule, Boolean.valueOf(result));
182        }
183        return result;
184    }
185
186    /**
187     * Returns true if document has one of the given facets, else false.
188     *
189     * @return true if document has one of the given facets, else false.
190     */
191    protected final boolean checkFacets(ActionContext context, String[] facets) {
192        DocumentModel doc = context.getCurrentDocument();
193        if (doc == null) {
194            return false;
195        }
196        for (String facet : facets) {
197            if (doc.hasFacet(facet)) {
198                if (log.isDebugEnabled()) {
199                    log.debug(String.format("#checkFacets: return true for facet '%s'", facet));
200                }
201                return true;
202            }
203        }
204        if (log.isDebugEnabled()) {
205            log.debug("#checkFacets: return false");
206        }
207        return false;
208    }
209
210    /**
211     * Returns true if given document has one of the permissions, else false.
212     * <p>
213     * If no document is found, return true only if principal is a manager.
214     *
215     * @return true if given document has one of the given permissions, else false
216     */
217    protected final boolean checkPermissions(ActionContext context, String[] permissions) {
218        DocumentModel doc = context.getCurrentDocument();
219        if (doc == null) {
220            NuxeoPrincipal principal = context.getCurrentPrincipal();
221            // default check when there is not context yet
222            if (principal != null) {
223                if (principal.isAdministrator()) {
224                    if (log.isDebugEnabled()) {
225                        log.debug("#checkPermissions: doc is null but user is admin => return true");
226                    }
227                    return true;
228                }
229            }
230            if (log.isDebugEnabled()) {
231                log.debug("#checkPermissions: doc and user are null => return false");
232            }
233            return false;
234        }
235        // check rights on doc
236        CoreSession docMgr = context.getDocumentManager();
237        if (docMgr == null) {
238            if (log.isDebugEnabled()) {
239                log.debug("#checkPermissions: no core session => return false");
240            }
241            return false;
242        }
243        for (String permission : permissions) {
244            if (docMgr.hasPermission(doc.getRef(), permission)) {
245                if (log.isDebugEnabled()) {
246                    log.debug(String.format("#checkPermissions: return true for permission '%s'", permission));
247                }
248                return true;
249            }
250        }
251        if (log.isDebugEnabled()) {
252            log.debug("#checkPermissions: return false");
253        }
254        return false;
255    }
256
257    protected final boolean checkGroups(ActionContext context, String[] groups) {
258        NuxeoPrincipal principal = context.getCurrentPrincipal();
259        if (principal == null) {
260            if (log.isDebugEnabled()) {
261                log.debug("#checkGroups: no user => return false");
262            }
263            return false;
264        }
265        for (String group : groups) {
266            if (principal.isMemberOf(group)) {
267                if (log.isDebugEnabled()) {
268                    log.debug(String.format("#checkGroups: return true for group '%s'", group));
269                }
270                return true;
271            }
272        }
273        if (log.isDebugEnabled()) {
274            log.debug("#checkGroups: return false");
275        }
276        return false;
277    }
278
279    /**
280     * Returns true if one of the conditions is verified, else false.
281     * <p>
282     * If one evaluation fails, return false.
283     *
284     * @return true if one of the conditions is verified, else false.
285     */
286    protected final boolean checkConditions(ActionContext context, String[] conditions) {
287        for (String condition : conditions) {
288            try {
289                if (context.checkCondition(condition)) {
290                    if (log.isDebugEnabled()) {
291                        log.debug(String.format("#checkCondition: return true for condition '%s'", condition));
292                    }
293                    return true;
294                }
295            } catch (ELException e) {
296                log.error("evaluation of condition " + condition + " failed: returning false", e);
297                return false;
298            }
299        }
300        if (log.isDebugEnabled()) {
301            log.debug("#checkConditions: return false");
302        }
303        return false;
304    }
305
306    /**
307     * Returns true if document type is one of the given types, else false.
308     * <p>
309     * If document is null, consider context is the server and return true if 'Server' is in the list.
310     *
311     * @return true if document type is one of the given types, else false.
312     */
313    protected final boolean checkTypes(ActionContext context, String[] types) {
314        DocumentModel doc = context.getCurrentDocument();
315        String docType;
316        if (doc == null) {
317            // consider we're on the Server root
318            docType = "Root";
319        } else {
320            docType = doc.getType();
321        }
322
323        for (String type : types) {
324            if (type.equals(docType)) {
325                if (log.isDebugEnabled()) {
326                    log.debug(String.format("#checkTypes: return true for type '%s'", docType));
327                }
328                return true;
329            }
330        }
331        if (log.isDebugEnabled()) {
332            log.debug("#checkTypes: return false");
333        }
334        return false;
335    }
336
337    /**
338     * Returns true if document has one of the given schemas, else false.
339     *
340     * @return true if document has one of the given schemas, else false
341     */
342    protected final boolean checkSchemas(ActionContext context, String[] schemas) {
343        DocumentModel doc = context.getCurrentDocument();
344        if (doc == null) {
345            if (log.isDebugEnabled()) {
346                log.debug("#checkSchemas: no doc => return false");
347            }
348            return false;
349        }
350        for (String schema : schemas) {
351            if (doc.hasSchema(schema)) {
352                if (log.isDebugEnabled()) {
353                    log.debug(String.format("#checkSchemas: return true for schema '%s'", schema));
354                }
355                return true;
356            }
357        }
358        if (log.isDebugEnabled()) {
359            log.debug("#checkSchemas: return false");
360        }
361        return false;
362    }
363
364    public boolean getAppend() {
365        return append;
366    }
367
368    public void setAppend(boolean append) {
369        this.append = append;
370    }
371
372    @Override
373    public DefaultActionFilter clone() {
374        DefaultActionFilter clone = new DefaultActionFilter();
375        clone.id = id;
376        clone.append = append;
377        if (rules != null) {
378            clone.rules = new FilterRule[rules.length];
379            for (int i = 0; i < rules.length; i++) {
380                clone.rules[i] = rules[i].clone();
381            }
382        }
383        return clone;
384    }
385
386    /**
387     * Equals method added to handle hot reload of inner filters, see NXP-9677
388     *
389     * @since 5.6
390     */
391    @Override
392    public boolean equals(Object obj) {
393        if (obj == null) {
394            return false;
395        }
396        if (this == obj) {
397            return true;
398        }
399        if (!(obj instanceof DefaultActionFilter)) {
400            return false;
401        }
402        final DefaultActionFilter o = (DefaultActionFilter) obj;
403        String objId = o.getId();
404        if (objId == null && !(this.id == null)) {
405            return false;
406        }
407        if (this.id == null && !(objId == null)) {
408            return false;
409        }
410        if (objId != null && !objId.equals(this.id)) {
411            return false;
412        }
413        boolean append = o.getAppend();
414        if (!append == this.append) {
415            return false;
416        }
417        FilterRule[] objRules = o.getRules();
418        if (objRules == null && !(this.rules == null)) {
419            return false;
420        }
421        if (this.rules == null && !(objRules == null)) {
422            return false;
423        }
424        if (objRules != null) {
425            if (objRules.length != this.rules.length) {
426                return false;
427            }
428            for (int i = 0; i < objRules.length; i++) {
429                if (objRules[i] == null && (!(this.rules[i] == null))) {
430                    return false;
431                }
432                if (this.rules[i] == null && (!(objRules[i] == null))) {
433                    return false;
434                }
435                if (!objRules[i].equals(this.rules[i])) {
436                    return false;
437                }
438            }
439        }
440        return true;
441    }
442}