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