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}