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}