/* * $Id$ * * Copyright 2006 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.security.basic; // Java imports import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.Assert; import ome.api.local.LocalAdmin; import ome.conditions.ApiUsageException; import ome.conditions.InternalException; import ome.conditions.SecurityViolation; import ome.model.IObject; import ome.model.enums.EventType; import ome.model.internal.Details; import ome.model.internal.Permissions; import ome.model.internal.Permissions.Right; import ome.model.internal.Permissions.Role; import ome.model.meta.Event; import ome.model.meta.EventLog; import ome.model.meta.Experimenter; import ome.model.meta.ExperimenterGroup; import ome.model.meta.Session; import ome.security.SecuritySystem; import ome.services.messages.RegisterServiceCleanupMessage; import ome.services.sessions.SessionContext; import ome.services.sessions.state.SessionCache; import ome.services.sessions.stats.SessionStats; import ome.services.sharing.ShareStore; import ome.services.util.ServiceHandler; import ome.system.EventContext; import ome.system.Principal; import ome.system.Roles; import ome.tools.hibernate.HibernateUtils; /** * Stores information related to the security context of the current thread. * Code calling into the server must setup CurrentDetails properly. An existing * user must be set (the creation of a new user is only allowed if the current * user is set to root; root always exists. QED.) The event must also be set. * Umask is optional. * * This information is stored in a Details object, but unlike Details which * assumes that an empty value signifies increased security levels, empty values * here signify reduced security levels. E.g., * * Details: user == null ==> object belongs to root CurrentDetails: user == null * ==> current user is "nobody" (anonymous) */ public class CurrentDetails implements PrincipalHolder { private static Logger log = LoggerFactory.getLogger(CurrentDetails.class); private final SessionCache cache; private final Roles roles; private final ThreadLocal<LinkedList<BasicEventContext>> contexts = new ThreadLocal<LinkedList<BasicEventContext>>(); /** * Call context set on the current details before login occurred. If this * is the case, then it will get consumed and */ private final ThreadLocal<Map<String, String>> delayedCallContext = new ThreadLocal<Map<String, String>>(); /** * Default constructor. Should only be used for testing, since the stats * used will not be correct. */ public CurrentDetails() { this.cache = null; this.roles = new Roles(); } public CurrentDetails(SessionCache cache) { this.cache = cache; this.roles = new Roles(); } public CurrentDetails(SessionCache cache, Roles roles) { this.cache = cache; this.roles = roles; } private LinkedList<BasicEventContext> list() { LinkedList<BasicEventContext> list = contexts.get(); if (list == null) { list = new LinkedList<BasicEventContext>(); contexts.set(list); } return list; } // Method call context methods // ================================================================= public Map<String, String> setContext(Map<String, String> ctx) { LinkedList<BasicEventContext> list = list(); if (list.size() == 0) { delayedCallContext.set(ctx); return null; } else { return list.getLast().setCallContext(ctx); } } public Map<String, String> getContext() { return list().getLast().getCallContext(); } protected void checkDelayedCallContext(BasicEventContext bec) { Map<String, String> ctx = delayedCallContext.get(); delayedCallContext.set(null); bec.setCallContext(ctx); } // PrincipalHolder methods // ================================================================= public int size() { return list().size(); } public Principal getLast() { return list().getLast().getPrincipal(); } public void login(Principal principal) { // Can't use the method in SessionManager since that leads to a // circular reference in Spring. final String uuid = principal.getName(); final SessionContext ctx = cache.getSessionContext(uuid); final SessionStats stats = ctx.stats(); final BasicEventContext c = new BasicEventContext(principal, stats); login(c); } /** * Login method which can be used by the security system to replace the * existing {@link BasicEventContext}. */ public void login(BasicEventContext bec) { if (log.isDebugEnabled()) { log.debug("Logging in :" + bec); } checkDelayedCallContext(bec); list().add(bec); bec.getStats().methodIn(); } public int logout() { LinkedList<BasicEventContext> list = list(); BasicEventContext bec = list.removeLast(); bec.getStats().methodOut(); if (log.isDebugEnabled()) { log.debug("Logged out: " + bec); } return list.size(); } // High-level methods used to fulfill {@link SecuritySystem} // ================================================================= /** * Checks if the current {@link Thread} has non-null {@link Experimenter}, * {@link Event}, and {@link ExperimenterGroup}, required for proper * functioning of the security system. */ public boolean isReady() { BasicEventContext c = current(); if (c.getEvent() != null && c.getGroup() != null && c.getOwner() != null) { return true; } return false; } public boolean isGraphCritical(Details details) { EventContext ec = getCurrentEventContext(); long gid = ec.getCurrentGroupId(); Permissions perms = ec.getCurrentGroupPermissions(); if (gid < 0) { try { ExperimenterGroup g = details.getGroup(); gid = g.getId(); perms = g.getDetails().getPermissions(); } catch (NullPointerException npe) { throw new SecurityViolation("isGraphCriticalCheck: not enough context"); } if (gid == roles.getUserGroupId()) { throw new SecurityViolation( "isGraphCriticalCheck: Current group < 0 while accessing 'user' group!"); } } boolean admin = ec.isCurrentUserAdmin(); boolean pi = ec.getLeaderOfGroupsList().contains(gid); if (perms.isGranted(Role.WORLD, Right.READ)) { // Public groups (rwrwrw) are always non-critical return false; } else if (perms.isGranted(Role.GROUP, Right.READ)) { // Since the object will be contained in the group, // then it will be readable regardless. return false; } else { // This is a private group. Any form of admin modification is // critical. return admin || pi; } } public boolean isOwnerOrSupervisor(IObject object) { if (object == null) { throw new ApiUsageException("Object can't be null"); } final Long o = HibernateUtils.nullSafeOwnerId(object); final Long g; // see 2874 and chmod if (object instanceof ExperimenterGroup) { g = object.getId(); } else { g = HibernateUtils.nullSafeGroupId(object); } final EventContext ec = getCurrentEventContext(); final boolean isAdmin = ec.isCurrentUserAdmin(); final boolean isPI = ec.getLeaderOfGroupsList().contains(g); final boolean isOwner = ec.getCurrentUserId().equals(o); if (isAdmin || isPI || isOwner) { return true; } return false; } // State management // ================================================================= /** * Replaces all the simple-valued fields in the {@link BasicEventContext}. * This method */ void checkAndInitialize(EventContext ec, LocalAdmin admin, ShareStore store) { current().checkAndInitialize(ec, admin, store); } /** * Returns the current {@link BasicEventContext instance} throwing an * exception if there isn't one. */ BasicEventContext current() { BasicEventContext c = list().getLast(); return c; } /** * @return the current event context */ public EventContext getCurrentEventContext() { return current(); } /** * It suffices to set the {@link Details} to a new instance to make this * context unusable. {@link #isReady()} will return false. */ public void invalidateCurrentEventContext() { BasicEventContext c = current(); c.invalidate(); if (log.isDebugEnabled()) { log.debug("Invalidated login: " + c); } } // ~ Events and Details // ================================================================= public Event newEvent(Session session, EventType type, TokenHolder tokenHolder) { BasicEventContext c = current(); Event e = new Event(); e.setType(type); e.setTime(new Timestamp(System.currentTimeMillis())); tokenHolder.setToken(e.getGraphHolder()); e.getDetails().setPermissions(Permissions.READ_ONLY); // Proxied if necessary e.setExperimenter(c.getOwner()); e.setExperimenterGroup(c.getGroup()); e.setSession(session); c.setEvent(e); return e; } public void addLog(String action, Class klass, Long id) { Assert.notNull(action); Assert.notNull(klass); Assert.notNull(id); if (Event.class.isAssignableFrom(klass) || EventLog.class.isAssignableFrom(klass)) { if (log.isDebugEnabled()) { log.debug("Not logging creation of logging type:" + klass); } return; // EARLY EXIT } else { if (!isReady()) { throw new InternalException("Not ready to add EventLog"); } } if (log.isInfoEnabled()) { log.info("Adding log:" + action + "," + klass + "," + id); } BasicEventContext c = current(); List<EventLog> list = current().getLogs(); if (list == null) { list = new ArrayList<EventLog>(); c.setLogs(list); } EventLog l = new EventLog(); l.setAction(action); l.setEntityType(klass.getName()); // TODO could be id to Type entity l.setEntityId(id); l.setEvent(c.getEvent()); Details d = Details.create(); d.setPermissions(Permissions.WORLD_IMMUTABLE); l.getDetails().copy(d); list.add(l); } public SessionStats getStats() { return current().getStats(); } public List<EventLog> getLogs() { // TODO defensive copy List<EventLog> logs = current().getLogs(); return logs == null ? new ArrayList<EventLog>() : logs; } public void clearLogs() { current().setLogs(null); } /** * Creates a {@link Details} object for the current security context. * * The {@link Permissions} on the instance are calculated from the current * group as well as the user's umask. * * @return details for the current security context * @see <a href="https://trac.openmicroscopy.org.uk/trac/omero/ticket:1434">ticket:1434</a> */ public Details createDetails() { final BasicEventContext c = current(); final Details d = Details.create(new Object[]{c, c.getCallContext()}); d.setCreationEvent(c.getEvent()); d.setUpdateEvent(c.getEvent()); d.setOwner(c.getOwner()); d.setGroup(c.getGroup()); // ticket:1434 final Permissions groupPerms = c.getCurrentGroupPermissions(); final Permissions p = new Permissions(groupPerms); d.setPermissions(p); return d; } public void applyContext(Details details, boolean changePerms) { final BasicEventContext c = current(); details.setContexts(new Object[]{c, c.getCallContext()}); if (changePerms) { // Make the permissions match (#8277) final Permissions groupPerms = c.getCurrentGroupPermissions(); if (groupPerms != Permissions.DUMMY) { details.setPermissions(new Permissions(groupPerms)); } else { // In the case of the dummy, we will be required to have // the group id already set in the context. ExperimenterGroup group = details.getGroup(); if (group != null) { // Systypes still will have DUMMY values. Long gid = details.getGroup().getId(); Permissions p = c.getPermissionsForGroup(gid); if (p != null) { // Ticket:9505. This must be a new copy of the permissions // in order to prevent the restrictions being modified by // later objects! details.setPermissions(new Permissions(p)); } else if (gid.equals(Long.valueOf(roles.getUserGroupId()))) { details.setPermissions(new Permissions(Permissions.EMPTY)); } else { throw new InternalException("No permissions: " + details); } } } } } /** * Checks the "groupPermissions" map in {@link BasicEventContext} which has * been filled up by calls to {@link BasicEventContext#setPermissionsForGroup(Long, Permissions)} * during {@link BasicACLVoter#allowLoad(org.hibernate.Session, Class, Details, long)}. * @param session the Hibernate session */ public void loadPermissions(org.hibernate.Session session) { current().loadPermissions(session); } public Experimenter getOwner() { return current().getOwner(); } public ExperimenterGroup getGroup() { return current().getGroup(); } public Event getEvent() { return current().getEvent(); } /** * Take all values during loadEventContext() in one shot. Event is set * during {@link #newEvent(long, EventType, TokenHolder)} and possibly * updated via {@link #updateEvent(Event)}. */ void setValues(Experimenter owner, ExperimenterGroup group, Permissions perms, boolean isAdmin, boolean isReadOnly, Long shareId) { BasicEventContext c = current(); c.setOwner(owner); c.setGroup(group, perms); c.setAdmin(isAdmin); c.setReadOnly(isReadOnly); c.setShareId(shareId); } /** * Allows for updating the {@link Event} if the session is not readOnly. */ void updateEvent(Event event) { current().setEvent(event); } // ~ Cleanups // ========================================================================= /** * Add a {@link RegisterServiceCleanupMessage} to the current thread for * cleanup by the {@link ServiceHandler} on exit. */ public void addCleanup(RegisterServiceCleanupMessage cleanup) { Set<RegisterServiceCleanupMessage> cleanups = current() .getServiceCleanups(); if (cleanups == null) { cleanups = new HashSet<RegisterServiceCleanupMessage>(); current().setServiceCleanups(cleanups); } cleanups.add(cleanup); } /** * Returns the current cleanups and resets the {@link Set}. Instances can * most likely only be closed once, so it doesn't make sense to keep them * around. The first caller of this method is responsible for closing all of * them. * @return a new copy of the current cleanups */ public Set<RegisterServiceCleanupMessage> emptyCleanups() { Set<RegisterServiceCleanupMessage> set = current().getServiceCleanups(); if (current().getServiceCleanups() == null) { return Collections.emptySet(); } else { Set<RegisterServiceCleanupMessage> copy = new HashSet<RegisterServiceCleanupMessage>( set); set.clear(); return copy; } } // ~ Subsystems // ========================================================================= public boolean addDisabled(String id) { Set<String> s = current().getDisabledSubsystems(); if (s == null) { s = new HashSet<String>(); current().setDisabledSubsystems(s); } return s.add(id); } public boolean addAllDisabled(String... ids) { Set<String> s = current().getDisabledSubsystems(); if (s == null) { s = new HashSet<String>(); current().setDisabledSubsystems(s); } if (ids != null) { return Collections.addAll(s, ids); } return false; } public boolean removeDisabled(String id) { Set<String> s = current().getDisabledSubsystems(); if (s != null && id != null) { return s.remove(id); } return false; } public boolean removeAllDisabled(String... ids) { Set<String> s = current().getDisabledSubsystems(); if (s != null && ids != null) { boolean changed = false; for (String string : ids) { changed |= s.remove(string); } } return false; } public void clearDisabled() { current().setDisabledSubsystems(null); } public boolean isDisabled(String id) { if (size() == 0) { // The security system is not active, so nothing can have // been "disabled" return false; } else { Set<String> s = current().getDisabledSubsystems(); if (s == null || id == null || !s.contains(id)) { return false; } return true; } } }