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}