/* * ome.security.basic.BasicSecuritySystem * * Copyright 2006 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.security.basic; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import ome.api.local.LocalAdmin; import ome.api.local.LocalQuery; import ome.api.local.LocalUpdate; import ome.conditions.ApiUsageException; import ome.conditions.InternalException; import ome.conditions.SecurityViolation; import ome.conditions.SessionTimeoutException; import ome.model.IObject; import ome.model.enums.EventType; import ome.model.internal.Details; import ome.model.internal.GraphHolder; import ome.model.internal.Permissions; import ome.model.internal.Permissions.Right; import ome.model.internal.Permissions.Role; import ome.model.internal.Token; import ome.model.meta.Event; import ome.model.meta.EventLog; import ome.model.meta.Experimenter; import ome.model.meta.ExperimenterGroup; import ome.model.meta.GroupExperimenterMap; import ome.security.AdminAction; import ome.security.SecureAction; import ome.security.SecurityFilter; import ome.security.SecurityFilterHolder; import ome.security.SecuritySystem; import ome.security.SystemTypes; import ome.security.policy.DefaultPolicyService; import ome.security.policy.PolicyService; import ome.services.messages.EventLogMessage; import ome.services.messages.EventLogsMessage; import ome.services.sessions.SessionManager; import ome.services.sessions.events.UserGroupUpdateEvent; import ome.services.sessions.state.SessionCache; import ome.services.sessions.stats.PerSessionStats; import ome.services.sharing.ShareStore; import ome.system.EventContext; import ome.system.OmeroContext; import ome.system.Principal; import ome.system.Roles; import ome.system.ServiceFactory; import ome.tools.hibernate.ExtendedMetadata; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.proxy.HibernateProxy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.orm.hibernate3.HibernateCallback; import org.springframework.util.Assert; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; /** * simplest implementation of {@link SecuritySystem}. Uses an ctor-injected * {@link EventContext} and the {@link ThreadLocal ThreadLocal-}based * {@link CurrentDetails} to provide the security infrastructure. * * @author Josh Moore, josh.moore at gmx.de * @see Token * @see SecuritySystem * @see Details * @see Permissions * @since 3.0-M3 */ public class BasicSecuritySystem implements SecuritySystem, ApplicationContextAware, ApplicationListener<EventLogMessage> { private final static Logger log = LoggerFactory.getLogger(BasicSecuritySystem.class); protected final OmeroInterceptor interceptor; protected final SystemTypes sysTypes; protected final CurrentDetails cd; protected final TokenHolder tokenHolder; protected final Roles roles; protected final SessionManager sessionManager; protected final ServiceFactory sf; protected final SecurityFilter filter; protected final PolicyService policyService; protected/* final */OmeroContext ctx; protected/* final */ShareStore store; /** * Simplified factory method which generates all the security primitives * internally. Primarily useful for generated testing instances. * @param sm the session manager * @param sf the session factory * @param cache the session cache * @return a configured security system */ public static BasicSecuritySystem selfConfigure(SessionManager sm, ServiceFactory sf, SessionCache cache) { CurrentDetails cd = new CurrentDetails(cache); SystemTypes st = new SystemTypes(); TokenHolder th = new TokenHolder(); OmeroInterceptor oi = new OmeroInterceptor(new Roles(), st, new ExtendedMetadata.Impl(), cd, th, new PerSessionStats(cd)); Roles roles = new Roles(); SecurityFilterHolder holder = new SecurityFilterHolder( cd, new OneGroupSecurityFilter(roles), new AllGroupsSecurityFilter(null, roles), new SharingSecurityFilter(roles, null)); BasicSecuritySystem sec = new BasicSecuritySystem(oi, st, cd, sm, roles, sf, new TokenHolder(), holder, new DefaultPolicyService()); return sec; } /** * Main public constructor for this {@link SecuritySystem} implementation. * @param interceptor the OMERO interceptor for Hibernate * @param sysTypes the system types * @param cd the current details * @param sessionManager the session manager * @param roles the OMERO roles * @param sf the session factory * @param tokenHolder the token holder * @param filter the security filter * @param policyService the policy service */ public BasicSecuritySystem(OmeroInterceptor interceptor, SystemTypes sysTypes, CurrentDetails cd, SessionManager sessionManager, Roles roles, ServiceFactory sf, TokenHolder tokenHolder, SecurityFilter filter, PolicyService policyService) { this.sessionManager = sessionManager; this.policyService = policyService; this.tokenHolder = tokenHolder; this.interceptor = interceptor; this.sysTypes = sysTypes; this.filter = filter; this.roles = roles; this.cd = cd; this.sf = sf; } public void setApplicationContext(ApplicationContext arg0) throws BeansException { this.ctx = (OmeroContext) arg0; this.store = this.ctx.getBean("shareStore", ShareStore.class); } // ~ Login/logout // ========================================================================= public void login(Principal principal) { cd.login(principal); } public int logout() { return cd.logout(); } // ~ Checks // ========================================================================= /** * implements {@link SecuritySystem#isReady()}. Simply checks for null * values in all the relevant fields of {@link CurrentDetails} */ public boolean isReady() { return cd.isReady(); } /** * classes which cannot be created by regular users. * * @see <a * href="http://trac.openmicroscopy.org.uk/ome/ticket/156">ticket156</a> */ public boolean isSystemType(Class<? extends IObject> klass) { return sysTypes.isSystemType(klass); } /** * tests whether or not the current user is either the owner of this entity, * or the superivsor of this entity, for example as root or as group owner. * * @param iObject * Non-null managed entity. * @return true if the current user is owner or supervisor of this entity */ public boolean isOwnerOrSupervisor(IObject iObject) { return cd.isOwnerOrSupervisor(iObject); } // ~ Read security // ========================================================================= /** * enables the read filter such that graph queries will have non-visible * entities silently removed from the return value. This filter does <em> * not</em> * apply to single value loads from the database. See * {@link ome.security.ACLVoter#allowLoad(Session, Class, Details, long)} for more. * * Note: this filter must be disabled on logout, otherwise the necessary * parameters (current user, current group, etc.) for building the filters * will not be available. Similarly, while enabling this filter, no calls * should be made on the given session object. * * @param session * a generic session object which can be used to enable this * filter. Each {@link SecuritySystem} implementation will * require a specific session type. * @see EventHandler#invoke(org.aopalliance.intercept.MethodInvocation) */ public void enableReadFilter(Object session) { if (session == null || !(session instanceof Session)) { throw new ApiUsageException( "The Object argument to enableReadFilter" + " in the BasicSystemSecurity implementation must be a " + " non-null org.hibernate.Session."); } checkReady("enableReadFilter"); // beware // http://opensource.atlassian.com/projects/hibernate/browse/HHH-1932 final EventContext ec = getEventContext(); final Session sess = (Session) session; filter.enable(sess, ec); } public void updateReadFilter(Session session) { filter.disable(session); enableReadFilter(session); } /** * disable this filer. All future queries will have no security context * associated with them and all items will be visible. * * @param session * a generic session object which can be used to disable this * filter. Each {@link SecuritySystem} implementation will * require a specifc session type. * @see EventHandler#invoke(org.aopalliance.intercept.MethodInvocation) */ public void disableReadFilter(Object session) { // Session system doesn't seem to provide this // i.e. isReady() is false here. Disabling but need to review // checkReady("disableReadFilter"); Session sess = (Session) session; filter.disable(sess); } // ~ Subsystem disabling // ========================================================================= public void disable(String... ids) { if (ids == null || ids.length == 0) { throw new ApiUsageException("Ids should not be empty."); } cd.addAllDisabled(ids); } public void enable(String... ids) { if (ids == null || ids.length == 0) { cd.clearDisabled(); } cd.removeAllDisabled(ids); } public boolean isDisabled(String id) { if (id == null) { throw new ApiUsageException("Id should not be null."); } return cd.isDisabled(id); } // OmeroInterceptor delegation // ========================================================================= public Details newTransientDetails(IObject object) throws ApiUsageException, SecurityViolation { checkReady("transientDetails"); return interceptor.newTransientDetails(object); } public Details checkManagedDetails(IObject object, Details trustedDetails) throws ApiUsageException, SecurityViolation { checkReady("managedDetails"); return interceptor.checkManagedDetails(object, trustedDetails); } // ~ CurrentDetails delegation (ensures proper settings of Tokens) // ========================================================================= public boolean isGraphCritical(Details details) { checkReady("isGraphCritical"); return cd.isGraphCritical(details); } public void loadEventContext(boolean isReadOnly) { loadEventContext(isReadOnly, false); } public void loadEventContext(boolean isReadOnly, boolean isClose) { final LocalAdmin admin = (LocalAdmin) sf.getAdminService(); final LocalUpdate update = (LocalUpdate) sf.getUpdateService(); // Call to session manager throws an exception on failure final Principal p = clearAndCheckPrincipal(); // ticket:6639 - Rather than catch the RemoveSessionException // we are going to check the type of the context and if it // matches, then we know we should do no more loading. EventContext ec = cd.getCurrentEventContext(); if (ec instanceof BasicSecurityWiring.CloseOnNoSessionContext) { throw new SessionTimeoutException("closing", ec); } // ticket:1855 - Catching SessionTimeoutException in order to permit // the close of a stateful service. try { ec = sessionManager.getEventContext(p); } catch (SessionTimeoutException ste) { if (!isClose) { throw ste; } ec = (EventContext) ste.sessionContext; } // Refill current details cd.checkAndInitialize(ec, admin, store); ec = cd.getCurrentEventContext(); // Replace with callContext // Experimenter Experimenter exp; if (isReadOnly) { exp = new Experimenter(ec.getCurrentUserId(), false); } else { exp = admin.userProxy(ec.getCurrentUserId()); } tokenHolder.setToken(exp.getGraphHolder()); // isAdmin boolean isAdmin = false; for (long gid : ec.getMemberOfGroupsList()) { if (roles.getSystemGroupId() == gid) { isAdmin = true; break; } } // Active group - starting with #3529, the current group and the current // share values should be definitive as setting the context on // BasicEventContext will automatically update the global values. For // security reasons, we need only guarantee that non-admins are // actually members of the noted groups. // // Joined with public group block (ticket:1940) Long shareId = ec.getCurrentShareId(); Long groupId = ec.getCurrentGroupId(); ExperimenterGroup callGroup = null; ExperimenterGroup eventGroup = null; long eventGroupId; Permissions callPerms; // Code copied in SessionManagerImpl if (groupId >= 0) { // negative groupId means all member groups eventGroupId = groupId; callGroup = admin.groupProxy(groupId); eventGroup = callGroup; callPerms = callGroup.getDetails().getPermissions(); // tickets:2950, 1940, 3529 if (!isAdmin && !ec.getMemberOfGroupsList().contains(groupId)) { if (!callPerms.isGranted(Role.WORLD, Right.READ)) { throw new SecurityViolation(String.format( "User %s is not a member of group %s and cannot login", ec.getCurrentUserId(), groupId)); } } } else { List<Long> memList = ec.getMemberOfGroupsList(); eventGroupId = memList.get(0); if (eventGroupId == roles.getUserGroupId() && memList.size() > 1) { eventGroupId = memList.get(1); } log.debug("Choice for event group: " + eventGroupId); eventGroup = admin.getGroup(eventGroupId); callGroup = new ExperimenterGroup(groupId, false); callPerms = Permissions.DUMMY; } long sessionId = ec.getCurrentSessionId().longValue(); ome.model.meta.Session sess = null; if (isReadOnly) { sess = new ome.model.meta.Session(sessionId, false); } else { sess = sf.getQueryService().get(ome.model.meta.Session.class, sessionId); } tokenHolder.setToken(callGroup.getGraphHolder()); // In order to less frequently access the ThreadLocal in CurrentDetails // All properties are now set in one shot, except for Event. cd.setValues(exp, callGroup, callPerms, isAdmin, isReadOnly, shareId); // Event String t = p.getEventType(); if (t == null) { t = ec.getCurrentEventType(); } EventType type = new EventType(t); tokenHolder.setToken(type.getGraphHolder()); Event event = cd.newEvent(sess, type, tokenHolder); tokenHolder.setToken(event.getGraphHolder()); // If this event is not read only, then lets save this event to prevent // flushing issues later. if (!isReadOnly) { if (event.getExperimenterGroup().getId() < 0) { event.setExperimenterGroup(eventGroup); } cd.updateEvent(update.saveAndReturnObject(event)); // TODO use merge } } private Principal clearAndCheckPrincipal() { // clear even if this fails. (make SecuritySystem unusable) invalidateEventContext(); if (cd.size() == 0) { throw new SecurityViolation( "Principal is null. Not logged in to SecuritySystem."); } final Principal p = cd.getLast(); if (p.getName() == null) { throw new InternalException( "Principal.name is null. Security system failure."); } return p; } public void addLog(String action, Class klass, Long id) { cd.addLog(action, klass, id); } public List<EventLog> getLogs() { return cd.getLogs(); } public void clearLogs() { if (log.isDebugEnabled()) { log.debug("Clearing EventLogs."); } List<EventLog> logs = getLogs(); if (!logs.isEmpty()) { boolean foundAdminType = false; final Multimap<String, EventLog> map = ArrayListMultimap.create(); for (EventLog el : getLogs()) { String t = el.getEntityType(); if (Experimenter.class.getName().equals(t) || ExperimenterGroup.class.getName().equals(t) || GroupExperimenterMap.class.getName().equals(t)) { foundAdminType = true; } map.put(t, el); } if (ctx == null) { log.error("No context found for publishing"); } else { // publish message if administrative type is modified if (foundAdminType) { this.ctx.publishEvent(new UserGroupUpdateEvent(this)); } this.ctx.publishEvent(new EventLogsMessage(this, map)); } } cd.clearLogs(); } public void invalidateEventContext() { if (log.isDebugEnabled()) { log.debug("Invalidating current EventContext."); } cd.invalidateCurrentEventContext(); } // ~ Tokens & Actions // ========================================================================= /** * * It would be better to catch the * {@link SecureAction#updateObject(IObject...)} method in a try/finally block, * but since flush can be so poorly controlled that's not possible. instead, * we use the one time token which is removed this Object is checked for * {@link #hasPrivilegedToken(IObject) privileges}. * * @param objs * A managed (non-detached) entity. Not null. * @param action * A code-block that will be given the entity argument with a * {@link #hasPrivilegedToken(IObject)} privileged token}. */ public <T extends IObject> T doAction(SecureAction action, T... objs) { Assert.notNull(objs); Assert.notEmpty(objs); Assert.notNull(action); final LocalQuery query = (LocalQuery) sf.getQueryService(); final List<GraphHolder> ghs = new ArrayList<GraphHolder>(); for (T obj : objs) { // TODO inject if (obj.getId() != null && !query.contains(obj)) { throw new SecurityViolation("Services are not allowed to call " + "doAction() on non-Session-managed entities."); } // ticket:1794 - use of IQuery.get along with doAction() creates // two objects (outer proxy and inner target) and only the outer // proxy has its graph holder modified without this block, leading // to security violations on flush since no token is present. if (obj instanceof HibernateProxy) { HibernateProxy hp = (HibernateProxy) obj; IObject obj2 = (IObject) hp.getHibernateLazyInitializer().getImplementation(); ghs.add(obj2.getGraphHolder()); } // FIXME // Token oneTimeToken = new Token(); // oneTimeTokens.put(oneTimeToken); ghs.add(obj.getGraphHolder()); } // Holding onto the graph holders since they protect the access // to their tokens for (GraphHolder graphHolder : ghs) { tokenHolder.setToken(graphHolder); // oneTimeToken } T retVal; try { retVal = action.updateObject(objs); } finally { for (GraphHolder graphHolder : ghs) { tokenHolder.clearToken(graphHolder); } } return retVal; } /** * Calls {@link #runAsAdmin(AdminAction)} with a null-group id. */ public void runAsAdmin(final AdminAction action) { runAsAdmin(null, action); } /** * merge event is disabled for {@link #runAsAdmin(AdminAction)} because * passing detached (client-side) entities to this method is particularly * dangerous. */ public void runAsAdmin(final ExperimenterGroup group, final AdminAction action) { Assert.notNull(action); // Need to check here so that no exception is thrown // during the try block below checkReady("runAsAdmin"); final LocalQuery query = (LocalQuery) sf.getQueryService(); query.execute(new HibernateCallback() { public Object doInHibernate(Session session) throws HibernateException, SQLException { BasicEventContext c = cd.current(); boolean wasAdmin = c.isCurrentUserAdmin(); ExperimenterGroup oldGroup = c.getGroup(); try { c.setAdmin(true); if (group != null) { c.setGroup(group, group.getDetails().getPermissions()); } disable(MergeEventListener.MERGE_EVENT); enableReadFilter(session); action.runAsAdmin(); } finally { c.setAdmin(wasAdmin); if (group != null) { c.setGroup(oldGroup, oldGroup.getDetails().getPermissions()); } enable(MergeEventListener.MERGE_EVENT); enableReadFilter(session); // Now as non-admin } return null; } }); } /** * @see TokenHolder#copyToken(IObject, IObject) */ public void copyToken(IObject source, IObject copy) { tokenHolder.copyToken(source, copy); } /** * @see TokenHolder#hasPrivilegedToken(IObject) */ public boolean hasPrivilegedToken(IObject obj) { return tokenHolder.hasPrivilegedToken(obj); } public void checkRestriction(String name, IObject obj) { policyService.checkRestriction(name, obj); } // ~ Configured Elements // ========================================================================= public Roles getSecurityRoles() { return roles; } public EventContext getEventContext(boolean refresh) { EventContext ec = cd.getCurrentEventContext(); if (refresh) { String uuid = ec.getCurrentSessionUuid(); ec = sessionManager.reload(uuid); } return ec; } public EventContext getEventContext() { return getEventContext(false); } /** * Returns the Id of the currently logged in user. * Returns owner of the share while in share * @return See above. */ public Long getEffectiveUID() { final EventContext ec = getEventContext(); final Long shareId = ec.getCurrentShareId(); if (shareId != null) { if (shareId < 0) { return null; } ome.model.meta.Session s = sf.getQueryService().get( ome.model.meta.Session.class, shareId); return s.getOwner().getId(); } return ec.getCurrentUserId(); } // ~ Helpers // ========================================================================= /** * calls {@link #isReady()} and if not throws an {@link ApiUsageException}. * The {@link SecuritySystem} must be in a valid state to perform several * functions. */ protected void checkReady(String method) { if (!isReady()) { throw new ApiUsageException("The security system is not ready.\n" + "Cannot execute: " + method); } } public void onApplicationEvent(EventLogMessage elm) { if (elm != null) { for (Long id : elm.entityIds) { addLog(elm.action, elm.entityType, id); } } } }