/* * ome.security.BasicACLVoter * * Copyright 2006 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.security.basic; import static ome.model.internal.Permissions.Role.GROUP; import static ome.model.internal.Permissions.Role.USER; import static ome.model.internal.Permissions.Role.WORLD; import java.util.Set; import ome.conditions.GroupSecurityViolation; import ome.conditions.InternalException; import ome.conditions.SecurityViolation; import ome.model.IObject; import ome.model.internal.Details; import ome.model.internal.Permissions; import ome.model.internal.Permissions.Right; import ome.model.internal.Token; import ome.model.meta.Experimenter; import ome.model.meta.ExperimenterGroup; import ome.security.ACLVoter; import ome.security.SecurityFilter; import ome.security.SecuritySystem; import ome.security.SystemTypes; import ome.security.policy.PolicyService; import ome.system.EventContext; import ome.system.Roles; import org.hibernate.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; /** * * @author Josh Moore, josh.moore at gmx.de * @version $Revision$, $Date$ * @see Token * @see SecuritySystem * @see Details * @see Permissions * @since 3.0-M3 */ public class BasicACLVoter implements ACLVoter { /** * Simple enum to represent the interpretation of "WRITE" permissions. */ private static enum Scope { ANNOTATE(Right.ANNOTATE), DELETE(Right.WRITE), EDIT(Right.WRITE), LINK(Right.WRITE); final Right right; Scope(Right right) { this.right = right; } } private final static Logger log = LoggerFactory.getLogger(BasicACLVoter.class); protected final CurrentDetails currentUser; protected final SystemTypes sysTypes; protected final TokenHolder tokenHolder; protected final SecurityFilter securityFilter; protected final PolicyService policyService; protected final Roles roles; public BasicACLVoter(CurrentDetails cd, SystemTypes sysTypes, TokenHolder tokenHolder, SecurityFilter securityFilter, PolicyService policyService) { this(cd, sysTypes, tokenHolder, securityFilter, policyService, new Roles()); } public BasicACLVoter(CurrentDetails cd, SystemTypes sysTypes, TokenHolder tokenHolder, SecurityFilter securityFilter, PolicyService policyService, Roles roles) { this.currentUser = cd; this.sysTypes = sysTypes; this.securityFilter = securityFilter; this.tokenHolder = tokenHolder; this.roles = roles; this.policyService = policyService; } // ~ Interface methods // ========================================================================= /** * */ public boolean allowChmod(IObject iObject) { return currentUser.isOwnerOrSupervisor(iObject); } /** * delegates to SecurityFilter because that is where the logic is defined * for the {@link BasicSecuritySystem#enableReadFilter(Object) read filter} * * Ignores the id for the moment. * * Though we pass in whether or not a share is active for completeness, a * different {@link ACLVoter} implementation will almost certainly be active * for share use. */ public boolean allowLoad(Session session, Class<? extends IObject> klass, Details d, long id) { Assert.notNull(klass); if (d == null || sysTypes.isSystemType(klass)) { // Here we're returning true because there // will be no group value that we can use // to store any permissions and don't want // WARNS in the log. return true; // EARLY EXIT! } boolean rv = false; if (sysTypes.isInSystemGroup(d) || sysTypes.isInUserGroup(d)) { rv = true; } else { rv = securityFilter.passesFilter(session, d, currentUser.current()); } // Misusing this location to store the loaded objects perms for later. if (this.currentUser.getCurrentEventContext().getCurrentGroupId() < 0) { // For every object that gets loaded when omero.group = -1, we // cache it's permissions in the session context so that when the // session is over we can re-apply all the permissions. ExperimenterGroup g = d.getGroup(); if (g == null) { log.warn(String.format("Group null while loading %s:%s", klass.getName(), id)); } if (g != null) { // Null for system types Long gid = g.getId(); Permissions p = g.getDetails().getPermissions(); if (p == null) { log.warn(String.format("Permissions null for group %s " + "while loading %s:%s", gid, klass.getName(), id)); } else { this.currentUser.current().setPermissionsForGroup(gid, p); } } } return rv; } public void throwLoadViolation(IObject iObject) throws SecurityViolation { Assert.notNull(iObject); throw new SecurityViolation("Cannot read " + iObject); } public boolean allowCreation(IObject iObject) { Assert.notNull(iObject); Class<?> cls = iObject.getClass(); boolean sysType = sysTypes.isSystemType(cls) || sysTypes.isInSystemGroup(iObject.getDetails()); // Note: removed restriction from #1769 that admins can only // create objects belonging to the current user. Instead, // OmeroInterceptor checks whether or not objects are only // LINKED to one's own objects which is the actual intent. if (tokenHolder.hasPrivilegedToken(iObject) || currentUser.getCurrentEventContext().isCurrentUserAdmin()) { return true; } else if (sysType) { return false; } return true; } public void throwCreationViolation(IObject iObject) throws SecurityViolation { Assert.notNull(iObject); boolean sysType = sysTypes.isSystemType(iObject.getClass()) || sysTypes.isInSystemGroup(iObject.getDetails()); if (!sysType && currentUser.isGraphCritical(iObject.getDetails())) { // ticket:1769 throw new GroupSecurityViolation(iObject + "-insertion violates " + "group-security."); } throw new SecurityViolation(iObject + " is a System-type, and may only be " + "created through privileged APIs."); } public boolean allowAnnotate(IObject iObject, Details trustedDetails) { BasicEventContext c = currentUser.current(); return 1 == allowUpdateOrDelete(c, iObject, trustedDetails, Scope.ANNOTATE); } public boolean allowUpdate(IObject iObject, Details trustedDetails) { BasicEventContext c = currentUser.current(); return 1 == allowUpdateOrDelete(c, iObject, trustedDetails, Scope.EDIT); } public void throwUpdateViolation(IObject iObject) throws SecurityViolation { Assert.notNull(iObject); boolean sysType = sysTypes.isSystemType(iObject.getClass()) || sysTypes.isInSystemGroup(iObject.getDetails()); if (!sysType && currentUser.isGraphCritical(iObject.getDetails())) { // ticket:1769 throw new GroupSecurityViolation(iObject +"-modification violates " + "group-security."); } throw new SecurityViolation("Updating " + iObject + " not allowed."); } public boolean allowDelete(IObject iObject, Details trustedDetails) { BasicEventContext c = currentUser.current(); return 1 == allowUpdateOrDelete(c, iObject, trustedDetails, Scope.DELETE); } public void throwDeleteViolation(IObject iObject) throws SecurityViolation { Assert.notNull(iObject); throw new SecurityViolation("Deleting " + iObject + " not allowed."); } boolean owner(Long o, EventContext c) { return (o != null && o.equals(c.getCurrentUserId())); } boolean owner(Details d, EventContext c) { Long o = d.getOwner() == null ? null : d.getOwner().getId(); return (o != null && o.equals(c.getCurrentUserId())); } boolean member(Long g, EventContext c) { return (g != null && c.getMemberOfGroupsList().contains(g)); } boolean member(Details d, EventContext c) { Long g = d.getGroup() == null ? null : d.getGroup().getId(); return member(g, c); } boolean leader(Long g, EventContext c) { return (g != null && c.getLeaderOfGroupsList().contains(g)); } boolean leader(Details d, EventContext c) { Long g = d.getGroup() == null ? null : d.getGroup().getId(); return leader(g, c); } /** * Determines whether or not the {@link Right} is available on this object * based on the ownership, group-membership, and group-permissions. * * Note: group leaders are automatically granted all rights. * * @param iObject * @param trustedDetails * @param update * @param right * @return an int with the bit turned on for each {@link Scope} element * which should be allowed. */ private int allowUpdateOrDelete(BasicEventContext c, IObject iObject, Details trustedDetails, Scope...scopes) { int rv = 0; if (iObject == null) { throw new IllegalArgumentException("null object"); } // Do not take the details directly from iObject // as it is in a critical state. Values such as // Permissions, owner, and group may have been changed. final Details d = trustedDetails; // this can now only happen if a table doesn't have permissions // and there aren't any of those. so let it be updated. if (d == null) { throw new InternalException("trustedDetails are null!"); } final boolean sysType = sysTypes.isSystemType(iObject.getClass()) || sysTypes.isInSystemGroup(d); final boolean sysTypeOrUsrGroup = sysType || sysTypes.isInUserGroup(d); // needs no details info if (tokenHolder.hasPrivilegedToken(iObject)) { return 1; // ticket:1794, allow move to "user } else if (!sysTypeOrUsrGroup && currentUser.isGraphCritical(d)) { //ticket:1769 Boolean belongs = null; final Long uid = c.getCurrentUserId(); for (int i = 0; i < scopes.length; i++) { if (scopes[i].equals(Scope.LINK) || scopes[i].equals(Scope.ANNOTATE)) { if (belongs == null) { belongs = objectBelongsToUser(iObject, uid); } // Cancel processing of this scope. rv is already 0 if (!belongs) { scopes[i] = null; } } } // Don't return. Need further processing for delete. } if (c.isCurrentUserAdmin()) { for (int i = 0; i < scopes.length; i++) { if (scopes[i] != null) { rv |= (1<<i); } } return rv; // EARLY EXIT! } else if (sysType) { return 0; } Permissions grpPermissions = c.getCurrentGroupPermissions(); if (grpPermissions == null || grpPermissions == Permissions.DUMMY) { if (d.getGroup() != null) { Long gid = d.getGroup().getId(); grpPermissions = c.getPermissionsForGroup(gid); if (grpPermissions == null && gid.equals(roles.getUserGroupId())) { grpPermissions = new Permissions(Permissions.EMPTY); } } if (grpPermissions == null) { throw new InternalException( "Permissions are null! Security system " + "failure -- refusing to continue. The Permissions should " + "be set to a default value."); } } final boolean owner = owner(d, c); final boolean leader = leader(d, c); final boolean member = member(d, c); for (int i = 0; i < scopes.length; i++) { Scope scope = scopes[i]; if (scope == null) continue; if (leader) { rv |= (1<<i); } // standard else if (grpPermissions.isGranted(WORLD, scope.right)) { rv |= (1<<i); } else if (owner && grpPermissions.isGranted(USER, scope.right)) { // Using cuId rather than getOwner since postProcess is also // post-login! rv |= (1<<i); } // Previously restricted by ticket:1992 // As of ticket:8562 this is handled by // the separation of ANNOTATE and WRITE else if (member && grpPermissions.isGranted(GROUP, scope.right) ) { rv |= (1<<i); } } return rv; // default was off, i.e. false } @Override public Set<String> restrictions(IObject object) { return policyService.listActiveRestrictions(object); } public void postProcess(IObject object) { if (object.isLoaded()) { Details details = object.getDetails(); // Sets context values.s this.currentUser.applyContext(details, !(object instanceof ExperimenterGroup)); final BasicEventContext c = currentUser.current(); final Permissions p = details.getPermissions(); final int allow = allowUpdateOrDelete(c, object, details, // This order must match the ordered of restrictions[] // expected by p.copyRestrictions Scope.LINK, Scope.EDIT, Scope.DELETE, Scope.ANNOTATE); // #9635 - This is not the most efficient solution // But since it's unclear why Permission objects // are currently being shared, the safest solution // is to always produce a copy. Permissions copy = new Permissions(p); copy.copyRestrictions(allow, restrictions(object)); details.setPermissions(copy); // #9635 } } /** * Check if the given object is owned by the given user. * @param iObject a model object * @param uid the ID of a user * @return if the object is owned by the user, or is not yet persisted */ // TODO this is less problematic than linking private boolean objectBelongsToUser(IObject iObject, Long uid) { final Experimenter e = iObject.getDetails().getOwner(); if (e == null) { if (iObject.getId() == null) { // ticket:8818 if this object does not yet have an ID // then we'll assume it's a newly created instance // which will eventually be saved with owner==uid return true; } throw new NullPointerException("Null owner for " + iObject); } Long oid = e.getId(); return uid.equals(oid); // Only allow own objects! } }