001/* 002 * (C) Copyright 2006-2018 Nuxeo (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 * Bogdan Stefanescu 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.core.api.security.impl; 021 022import java.io.IOException; 023import java.io.ObjectInputStream; 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029 030import org.apache.commons.lang3.StringUtils; 031import org.nuxeo.ecm.core.api.security.ACE; 032import org.nuxeo.ecm.core.api.security.ACL; 033import org.nuxeo.ecm.core.api.security.ACP; 034import org.nuxeo.ecm.core.api.security.Access; 035import org.nuxeo.ecm.core.api.security.SecurityConstants; 036import org.nuxeo.ecm.core.api.security.UserEntry; 037import org.nuxeo.runtime.api.Framework; 038import org.nuxeo.runtime.services.config.ConfigurationService; 039 040/** 041 * The ACP implementation uses a cache used when calling getAccess(). 042 */ 043public class ACPImpl implements ACP { 044 045 /** 046 * ConfigurationService property to enable legacy behavior. 047 * 048 * @since 10.2 049 */ 050 public static String LEGACY_BEHAVIOR_PROPERTY = "nuxeo.security.acl.legacyBehavior"; 051 052 private static final long serialVersionUID = 1L; 053 054 private final List<ACL> acls; 055 056 private transient Map<String, Access> cache; 057 058 private Boolean legacyBehavior; 059 060 public ACPImpl() { 061 acls = new ArrayList<>(); 062 cache = new HashMap<>(); 063 } 064 065 /** 066 * This method must append the ACL and not insert it since it is used to append the inherited ACL which is the less 067 * significant ACL. 068 */ 069 @Override 070 public void addACL(ACL acl) { 071 assert acl != null; 072 ACL oldACL = getACL(acl.getName()); 073 if (!acl.equals(oldACL)) { 074 // replace existing ACL instance different from acl having the same 075 // name, if any 076 if (oldACL != null) { 077 oldACL.clear(); 078 oldACL.addAll(acl); 079 } else { 080 String name = acl.getName(); 081 switch (name) { 082 case ACL.INHERITED_ACL: 083 // add the inherited ACL always at the end 084 acls.add(acl); 085 break; 086 case ACL.LOCAL_ACL: 087 // add the local ACL before the inherited if any 088 ACL inherited = getACL(ACL.INHERITED_ACL); 089 if (inherited != null) { 090 int i = acls.indexOf(inherited); 091 acls.add(i, acl); 092 } else { 093 acls.add(acl); 094 } 095 break; 096 default: 097 ACL local = getACL(ACL.LOCAL_ACL); 098 if (local != null) { 099 int i = acls.indexOf(local); 100 if (useLegacyBehavior()) { 101 i++; 102 } 103 acls.add(i, acl); 104 } else { 105 inherited = getACL(ACL.INHERITED_ACL); 106 if (inherited != null) { 107 int i = acls.indexOf(inherited); 108 acls.add(i, acl); 109 } else { 110 acls.add(acl); 111 } 112 } 113 } 114 } 115 } 116 // if oldACL and ACL are the same instance, we just need to clear 117 // the cache 118 cache.clear(); 119 } 120 121 @Override 122 public void addACL(int pos, ACL acl) { 123 ACL oldACL = getACL(acl.getName()); 124 if (oldACL != null) { 125 acls.remove(oldACL); 126 } 127 acls.add(pos, acl); 128 cache.clear(); 129 } 130 131 @Override 132 public void addACL(String afterMe, ACL acl) { 133 if (afterMe == null) { 134 addACL(0, acl); 135 } else { 136 int i; 137 int len = acls.size(); 138 for (i = 0; i < len; i++) { 139 if (acls.get(i).getName().equals(afterMe)) { 140 break; 141 } 142 } 143 addACL(i + 1, acl); 144 } 145 } 146 147 @Override 148 public ACL getACL(String name) { 149 String localName = name == null ? ACL.LOCAL_ACL : name; 150 return acls.stream().filter(acl -> acl.getName().equals(localName)).findFirst().orElse(null); 151 } 152 153 @Override 154 public ACL[] getACLs() { 155 return acls.toArray(new ACL[acls.size()]); 156 } 157 158 @Override 159 public ACL getMergedACLs(String name) { 160 ACL mergedAcl = new ACLImpl(name, true); 161 for (ACL acl : acls) { 162 mergedAcl.addAll(acl); 163 } 164 return mergedAcl; 165 } 166 167 public static ACL newACL(String name) { 168 return new ACLImpl(name); 169 } 170 171 @Override 172 public ACL removeACL(String name) { 173 for (int i = 0, len = acls.size(); i < len; i++) { 174 ACL acl = acls.get(i); 175 if (acl.getName().equals(name)) { 176 cache.clear(); 177 return acls.remove(i); 178 } 179 } 180 return null; 181 } 182 183 @Override 184 public Access getAccess(String principal, String permission) { 185 // check first the cache 186 String key = principal + ':' + permission; 187 Access access = cache.get(key); 188 if (access == null) { 189 access = Access.UNKNOWN; 190 FOUND_ACE: for (ACL acl : acls) { 191 for (ACE ace : acl) { 192 if (permissionsMatch(ace, permission) && principalsMatch(ace, principal)) { 193 access = ace.isGranted() ? Access.GRANT : Access.DENY; 194 break FOUND_ACE; 195 } 196 } 197 } 198 cache.put(key, access); 199 } 200 return access; 201 } 202 203 @Override 204 public Access getAccess(String[] principals, String[] permissions) { 205 for (ACL acl : acls) { 206 for (ACE ace : acl) { 207 // only check for effective ACEs 208 if (ace.isEffective()) { 209 // fully check ACE in turn against username/permissions 210 // and usergroups/permgroups 211 Access access = getAccess(ace, principals, permissions); 212 if (access != Access.UNKNOWN) { 213 return access; 214 } 215 } 216 } 217 } 218 return Access.UNKNOWN; 219 } 220 221 public static Access getAccess(ACE ace, String[] principals, String[] permissions) { 222 String acePerm = ace.getPermission(); 223 String aceUser = ace.getUsername(); 224 225 for (String principal : principals) { 226 if (principalsMatch(aceUser, principal)) { 227 // check permission match only if principal is matching 228 for (String permission : permissions) { 229 if (permissionsMatch(acePerm, permission)) { 230 return ace.isGranted() ? Access.GRANT : Access.DENY; 231 } // end permissionMatch 232 } // end perm for 233 } // end principalMatch 234 } // end princ for 235 return Access.UNKNOWN; 236 } 237 238 private static boolean permissionsMatch(ACE ace, String permission) { 239 String acePerm = ace.getPermission(); 240 241 // RESTRICTED_READ needs special handling, is not implied by EVERYTHING. 242 if (!SecurityConstants.RESTRICTED_READ.equals(permission)) { 243 if (SecurityConstants.EVERYTHING.equals(acePerm)) { 244 return true; 245 } 246 } 247 return StringUtils.equals(acePerm, permission); 248 } 249 250 private static boolean permissionsMatch(String acePerm, String permission) { 251 // RESTRICTED_READ needs special handling, is not implied by EVERYTHING. 252 if (SecurityConstants.EVERYTHING.equals(acePerm)) { 253 if (!SecurityConstants.RESTRICTED_READ.equals(permission)) { 254 return true; 255 } 256 } 257 return StringUtils.equals(acePerm, permission); 258 } 259 260 private static boolean principalsMatch(ACE ace, String principal) { 261 String acePrincipal = ace.getUsername(); 262 return principalsMatch(acePrincipal, principal); 263 } 264 265 private static boolean principalsMatch(String acePrincipal, String principal) { 266 return SecurityConstants.EVERYONE.equals(acePrincipal) || StringUtils.equals(acePrincipal, principal); 267 } 268 269 public void addAccessRule(String aclName, ACE ace) { 270 ACL acl = getACL(aclName); 271 if (acl == null) { 272 acl = new ACLImpl(aclName); 273 addACL(acl); 274 } 275 acl.add(ace); 276 } 277 278 @Override 279 public ACL getOrCreateACL(String name) { 280 ACL acl = getACL(name); 281 if (acl == null) { 282 acl = new ACLImpl(name); 283 addACL(acl); 284 } 285 return acl; 286 } 287 288 @Override 289 public ACL getOrCreateACL() { 290 return getOrCreateACL(ACL.LOCAL_ACL); 291 } 292 293 // Rules. 294 295 @Override 296 public void setRules(String aclName, UserEntry[] userEntries) { 297 setRules(aclName, userEntries, true); 298 } 299 300 @Override 301 public void setRules(String aclName, UserEntry[] userEntries, boolean overwrite) { 302 303 ACL acl = getACL(aclName); 304 if (acl == null) { // create the loca ACL 305 acl = new ACLImpl(aclName); 306 addACL(acl); 307 } else if (overwrite) { 308 // :XXX: Should not overwrite entries not given as parameters here. 309 acl.clear(); 310 } 311 for (UserEntry entry : userEntries) { 312 String username = entry.getUserName(); 313 for (String permission : entry.getGrantedPermissions()) { 314 acl.add(new ACE(username, permission, true)); 315 } 316 for (String permission : entry.getDeniedPermissions()) { 317 acl.add(new ACE(username, permission, false)); 318 } 319 } 320 cache.clear(); 321 } 322 323 @Override 324 public void setRules(UserEntry[] userEntries) { 325 setRules(ACL.LOCAL_ACL, userEntries); 326 } 327 328 @Override 329 public void setRules(UserEntry[] userEntries, boolean overwrite) { 330 setRules(ACL.LOCAL_ACL, userEntries, overwrite); 331 } 332 333 // Serialization. 334 335 private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException { 336 // always perform the default de-serialization first 337 in.defaultReadObject(); 338 // initialize cache to avoid NPE 339 cache = new HashMap<>(); 340 } 341 342 /* 343 * NXP-1822 Rux: method for validating in one shot the users allowed to perform an oration. It gets the list of 344 * individual permissions which supposedly all grant. 345 */ 346 @Override 347 public String[] listUsernamesForAnyPermission(Set<String> perms) { 348 List<String> usernames = new ArrayList<>(); 349 ACL merged = getMergedACLs("merged"); 350 for (ACE ace : merged.getACEs()) { 351 if (perms.contains(ace.getPermission()) && ace.isGranted()) { 352 String username = ace.getUsername(); 353 if (!usernames.contains(username)) { 354 usernames.add(username); 355 } 356 } 357 } 358 return usernames.toArray(new String[usernames.size()]); 359 } 360 361 @Override 362 public ACPImpl clone() { 363 ACPImpl copy = new ACPImpl(); 364 for (ACL acl : acls) { 365 copy.acls.add((ACL) acl.clone()); 366 } 367 return copy; 368 } 369 370 @Override 371 public boolean blockInheritance(String aclName, String username) { 372 if (aclName == null) { 373 throw new NullPointerException("'aclName' cannot be null"); 374 } 375 if (username == null) { 376 throw new NullPointerException("'username' cannot be null"); 377 } 378 379 ACL acl = getOrCreateACL(aclName); 380 boolean aclChanged = acl.blockInheritance(username); 381 if (aclChanged) { 382 addACL(acl); 383 } 384 return aclChanged; 385 } 386 387 @Override 388 public boolean unblockInheritance(String aclName) { 389 if (aclName == null) { 390 throw new NullPointerException("'aclName' cannot be null"); 391 } 392 393 ACL acl = getOrCreateACL(aclName); 394 boolean aclChanged = acl.unblockInheritance(); 395 if (aclChanged) { 396 addACL(acl); 397 } 398 return aclChanged; 399 } 400 401 @Override 402 public boolean addACE(String aclName, ACE ace) { 403 if (aclName == null) { 404 throw new NullPointerException("'aclName' cannot be null"); 405 } 406 407 ACL acl = getOrCreateACL(aclName); 408 boolean aclChanged = acl.add(ace); 409 if (aclChanged) { 410 addACL(acl); 411 } 412 return aclChanged; 413 } 414 415 @Override 416 public boolean replaceACE(String aclName, ACE oldACE, ACE newACE) { 417 if (aclName == null) { 418 throw new NullPointerException("'aclName' cannot be null"); 419 } 420 421 ACL acl = getOrCreateACL(aclName); 422 boolean aclChanged = acl.replace(oldACE, newACE); 423 if (aclChanged) { 424 addACL(acl); 425 } 426 return aclChanged; 427 } 428 429 @Override 430 public boolean removeACE(String aclName, ACE ace) { 431 if (aclName == null) { 432 throw new NullPointerException("'aclName' cannot be null"); 433 } 434 435 ACL acl = getOrCreateACL(aclName); 436 boolean aclChanged = acl.remove(ace); 437 if (aclChanged) { 438 addACL(acl); 439 } 440 return aclChanged; 441 } 442 443 @Override 444 public boolean removeACEsByUsername(String aclName, String username) { 445 if (aclName == null) { 446 throw new NullPointerException("'aclName' cannot be null"); 447 } 448 449 ACL acl = getOrCreateACL(aclName); 450 boolean aclChanged = acl.removeByUsername(username); 451 if (aclChanged) { 452 addACL(acl); 453 } 454 return aclChanged; 455 } 456 457 @Override 458 public boolean removeACEsByUsername(String username) { 459 boolean changed = false; 460 for (ACL acl : acls) { 461 boolean aclChanged = acl.removeByUsername(username); 462 if (aclChanged) { 463 addACL(acl); 464 changed = true; 465 } 466 } 467 return changed; 468 } 469 470 @SuppressWarnings("AutoBoxing") 471 protected boolean useLegacyBehavior() { 472 if (legacyBehavior == null) { 473 // check runtime is present - as ACP is a simple object, it could be used outside of a runtime context 474 // otherwise don't use legacy behavior 475 legacyBehavior = Framework.getRuntime() != null 476 && Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(LEGACY_BEHAVIOR_PROPERTY); 477 } 478 return legacyBehavior.booleanValue(); 479 } 480}