/* * Copyright 2007 Glencoe Software, Inc. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.services.sessions; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Future; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import ome.api.local.LocalAdmin; import ome.conditions.ApiUsageException; import ome.conditions.AuthenticationException; import ome.conditions.InternalException; import ome.conditions.RemovedSessionException; import ome.conditions.SecurityViolation; import ome.conditions.SessionException; import ome.conditions.SessionTimeoutException; import ome.model.annotations.Annotation; import ome.model.annotations.CommentAnnotation; import ome.model.annotations.TextAnnotation; import ome.model.enums.EventType; import ome.model.internal.Details; import ome.model.internal.Permissions; import ome.model.meta.Experimenter; import ome.model.meta.ExperimenterGroup; import ome.model.meta.Node; import ome.model.meta.Session; import ome.model.meta.Share; import ome.parameters.Filter; import ome.parameters.Parameters; import ome.security.basic.PrincipalHolder; import ome.services.messages.CreateSessionMessage; import ome.services.messages.DestroySessionMessage; import ome.services.sessions.events.ChangeSecurityContextEvent; import ome.services.sessions.events.UserGroupUpdateEvent; import ome.services.sessions.state.SessionCache; import ome.services.sessions.stats.CounterFactory; import ome.services.sessions.stats.SessionStats; import ome.services.util.Executor; import ome.services.util.Executor.Priority; import ome.system.EventContext; import ome.system.OmeroContext; import ome.system.Principal; import ome.system.Roles; import ome.system.ServiceFactory; import ome.util.SqlAction; import org.apache.commons.lang.StringUtils; 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.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.transaction.annotation.Transactional; import com.google.common.collect.MapMaker; /** * Is for ISession a cache and will be kept there in sync? OR Factors out the * logic from ISession and SessionManagerI * * Therefore either called directly, or via synchronous messages. * * Uses the name of a Principal as the key to the session. We may need to limit * user names to prevent this. (Strictly alphanumeric) * * Receives notifications as an {@link ApplicationListener}, which should be * used to keep the {@link Session} instances up-to-date. * * @author Josh Moore, josh at glencoesoftware.com * @since 3.0-Beta3 */ public class SessionManagerImpl implements SessionManager, SessionCache.StaleCacheListener, ApplicationContextAware, ApplicationListener<ApplicationEvent> { public final static String GROUP_SUDO_NS = "openmicroscopy.org/security/group-sudo"; private final static Logger log = LoggerFactory.getLogger(SessionManagerImpl.class); /** * The id of this session manager, used to identify its own actions. This * value may be overwritten by an injector with a value which is used * throughout this server instance. */ private String internal_uuid = UUID.randomUUID().toString(); // Injected protected OmeroContext context; protected Roles roles; protected SessionCache cache; protected Executor executor; protected long defaultTimeToIdle; protected long maxUserTimeToIdle; protected long defaultTimeToLive; protected long maxUserTimeToLive; protected PrincipalHolder principalHolder; protected CounterFactory factory; protected boolean readOnly = false; // Local state /** * A private session for use only by this instance for running methods via * {@link Executor}. The name of this {@link Principal} will not be removed * by calls to {@link #closeAll()}. */ protected Principal asroot; /** * Internal {@link SessionContext} created during {@link #init()} which is * used for all method calls internal to the session manager (see execute* * methods) */ protected SessionContext internalSession; // ~ Injectors // ========================================================================= public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = (OmeroContext) applicationContext; } public void setUuid(String uuid) { this.internal_uuid = uuid; } public void setSessionCache(SessionCache sessionCache) { cache = sessionCache; this.cache.setStaleCacheListener(this); } public void setRoles(Roles securityRoles) { roles = securityRoles; } public void setExecutor(Executor executor) { this.executor = executor; } public void setDefaultTimeToIdle(long defaultTimeToIdle) { this.defaultTimeToIdle = defaultTimeToIdle; this.maxUserTimeToIdle = Math.min(Long.MAX_VALUE / 10, defaultTimeToIdle); this.maxUserTimeToIdle *= 10; } public void setDefaultTimeToLive(long defaultTimeToLive) { this.defaultTimeToLive = defaultTimeToLive; this.maxUserTimeToLive = Math.min(Long.MAX_VALUE / 10, defaultTimeToLive); this.maxUserTimeToLive *= 10; } public void setPrincipalHolder(PrincipalHolder principal) { this.principalHolder = principal; } public void setCounterFactory(CounterFactory factory) { this.factory = factory; } public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } /** * Initialization method called by the Spring run-time to acquire an initial * {@link Session}. */ public void init() { try { asroot = new Principal(internal_uuid, "system", "Sessions"); final Session session = executeInternalSession(); internalSession = new InternalSessionContext(session, roles); cache.putSession(internal_uuid, internalSession); } catch (UncategorizedSQLException uncat) { log.warn("Assuming that this is read-only"); } catch (DataAccessException dataAccess) { throw new RuntimeException( " " + "=====================================================\n" + "Data access exception: Did you create your database? \n" + "=====================================================\n", dataAccess); } } // ~ Session definition // ========================================================================= protected void define(Session s, String uuid, String message, long started, CreationRequest req) { Long idle = req.timeToIdle == null ? defaultTimeToIdle : req.timeToIdle; Long live = req.timeToLive == null ? defaultTimeToLive : req.timeToLive; if (req.groupsLed != null) { CommentAnnotation ca = new CommentAnnotation(); ca.setNs(GROUP_SUDO_NS); StringBuilder sb = new StringBuilder(); for (int i = 0; i < req.groupsLed.size(); i++) { if (i > 0) { sb.append(","); } sb.append(req.groupsLed.get(i)); } ca.setTextValue(sb.toString()); s.linkAnnotation(ca); } define(s, uuid, message, started, idle, live, req.principal.getEventType(), req.agent, req.ip); } protected void define(Session s, String uuid, String message, long started, long idle, long live, String eventType, String agent, String ip) { s.getDetails().setPermissions(Permissions.PRIVATE); s.setUuid(uuid); s.setMessage(message); s.setStarted(new Timestamp(started)); s.setTimeToIdle(idle); s.setTimeToLive(live); s.setDefaultEventType(eventType); s.setUserAgent(agent); s.setUserIP(ip); } // ~ Session management // ========================================================================= public Session createFromRequest(CreationRequest request) { // If credentials exist as session, then return that if (request.credentials != null) { try { SessionContext context = cache .getSessionContext(request.credentials); if (context != null) { context.count().increment(); return context.getSession(); // EARLY EXIT! } } catch (SessionException se) { // oh well. } // Though trusted values, if we receive a null principal, not ok; boolean ok = request.principal == null ? false : executeCheckPassword( request.principal, request.credentials); if (!ok) { log.warn("Failed to authenticate: " + request.principal); throw new AuthenticationException("Authentication exception."); } } // authentication checked. Now delegating to the admin method (no pass) Session session = new Session(); define(session, UUID.randomUUID().toString(), "Initial message.", System.currentTimeMillis(), request); return createSession(request, session); } /*k * Is given trustable values by the {@link SessionBean} */ public Session createWithAgent(final Principal _principal, final String credentials, String agent, String ip) { final CreationRequest req = new CreationRequest(); req.principal = _principal; req.credentials = credentials; req.agent = agent; req.ip = ip; return createFromRequest(req); } public Session createWithAgent(Principal principal, String agent, String ip) { final CreationRequest req = new CreationRequest(); req.principal = principal; req.agent = agent; req.ip = ip; return createFromRequest(req); } public Share createShare(Principal principal, boolean enabled, long timeToLive, String eventType, String description, long groupId) { Share share = newShare(); define(share, UUID.randomUUID().toString(), description, System .currentTimeMillis(), defaultTimeToIdle, timeToLive, eventType, "Share", null); share.setGroup(new ExperimenterGroup(groupId, false)); share.setActive(enabled); share.setData(new byte[] {}); share.setItemCount(0L); CreationRequest req = new CreationRequest(); req.principal = principal; return (Share) createSession(req, share); } @SuppressWarnings("unchecked") private Session createSession(final CreationRequest req, final Session oldsession) { final Principal principal = req.principal; // If username exists as session, then return that try { SessionContext context = cache.getSessionContext(principal.getName()); if (context != null) { context.count().increment(); return context.getSession(); // EARLY EXIT! } } catch (SessionException se) { // oh well } List<Object> rv; Map<String, String> sysContext = new HashMap<String, String>(); sysContext.put("omero.group", Long.toString(roles.getSystemGroupId())); // No reason to perform this in any group other than system. if (readOnly) { rv = (List<Object>) executor.execute(sysContext, this.asroot, new Executor.SimpleWork( this, "read-only createSession") { @Transactional(readOnly = true) public Object doWork(org.hibernate.Session __s, ServiceFactory sf) { Principal p = validateSessionInputs(sf, req); executeLookupUser(sf, p); // Not performed! Session s = executeUpdate(sf, oldsession, // userId); Session s = oldsession; return executeSessionContextLookup(sf, p, s); } }); } else { rv = (List<Object>) executor.execute(sysContext, this.asroot, new Executor.SimpleWork( this, "createSession") { @Transactional(readOnly = false) public Object doWork(org.hibernate.Session __s, ServiceFactory sf) { Principal p = validateSessionInputs(sf, req); oldsession.setDefaultEventType(p.getEventType()); long userId = executeLookupUser(sf, p); Session s = executeUpdate(sf, oldsession, userId); return executeSessionContextLookup(sf, p, s); } }); } if (rv == null) { throw new RemovedSessionException("No info in database for " + principal); } SessionContext newctx = createSessionContext(rv, null); // This the publishEvent returns successfully, then we will have to // handle rolling back this addition our selves String uuid = newctx.getCurrentSessionUuid(); cache.putSession(uuid, newctx); try { context.publishEvent(new CreateSessionMessage(this, uuid)); } catch (RuntimeException re) { log.warn("Session creation cancelled by event listener", re); cache.removeSession(uuid); throw re; } // All successful, increment and return. newctx.count().increment(); return newctx.getSession(); } public Session update(Session session) { return update(session, false); } public Session update(final Session session, final boolean trusted) { if (session == null || !session.isLoaded() || session.getUuid() == null) { throw new RemovedSessionException("Cannot update; No uuid."); } final String uuid = session.getUuid(); final Details details = session.getDetails(); final SessionContext ctx = cache.getSessionContext(uuid); if (ctx == null) { throw new RemovedSessionException( "Can't update; No session with uuid:" + uuid); } final Session orig = ctx.getSession(); // TODO // FIXME // ===================================================== // This needs to get smarter @SuppressWarnings("unchecked") List<Object> list = (List<Object>) executor.execute(asroot, new Executor.SimpleWork( this, "load_for_update") { @Transactional(readOnly = false) public Object doWork(org.hibernate.Session __s, ServiceFactory sf) { // Allow user to change default group String defaultGroup = null; if (details != null) { ExperimenterGroup group = details.getGroup(); if (group != null) { try { Long groupId = group.getId(); if (groupId != null) { group = ((LocalAdmin) sf.getAdminService()) .groupProxy(groupId); if (group != null) { defaultGroup = group.getName(); } } } catch (Exception e) { throw new ApiUsageException( "Cannot change default group to " + group + "\n" + e.getMessage()); } } } // If still null, take the current if (defaultGroup == null) { defaultGroup = ctx.getCurrentGroupName(); } Principal principal = new Principal(ctx.getCurrentUserName(), defaultGroup, ctx.getCurrentEventType()); CreationRequest req = new CreationRequest(); req.principal = principal; principal = validateSessionInputs(sf, req); // Unconditionally settable; these are open to the user for // change parseAndSetDefaultType(session.getDefaultEventType(), orig); parseAndSetUserAgent(session.getUserAgent(), orig); // Conditionally settable parseAndSetTimeouts(session.getTimeToLive(), session .getTimeToIdle(), orig, trusted); // TODO Need to handle notifications return executeSessionContextLookup(sf, principal, orig); } }); if (list == null) { log.info("removeSession on update: " + uuid); cache.removeSession(uuid); throw new RemovedSessionException("Database contains no info for " + uuid); } ; final SessionContext newctx = createSessionContext(list, ctx); final Session copy = copy(orig); executor.execute(asroot, new Executor.SimpleWork(this, "update") { @Transactional(readOnly = false) public Object doWork(org.hibernate.Session __s, ServiceFactory sf) { return executeUpdate(sf, copy, newctx.getCurrentUserId()); } }); cache.putSession(uuid, newctx); return copy(orig); } /** * Takes a snapshot as from * {@link #executeSessionContextLookup(ServiceFactory, Principal, Session)} * and turns it into a {@link SessionContext} instance. * List argument should never be null. Abort if * {@link #executeSessionContextLookup(ServiceFactory, Principal, Session)} * returns null. */ @SuppressWarnings("unchecked") protected SessionContext createSessionContext(List<?> list, SessionContext previous) { final Experimenter exp = (Experimenter) list.get(0); final ExperimenterGroup grp = (ExperimenterGroup) list.get(1); final List<Long> memberOfGroupsIds = (List<Long>) list.get(2); final List<Long> leaderOfGroupsIds = (List<Long>) list.get(3); final List<String> userRoles = (List<String>) list.get(4); final Principal principal = (Principal) list.get(5); final Session session = (Session) list.get(6); parseAndSetDefaultType(principal.getEventType(), session); session.getDetails().setOwner(exp); session.getDetails().setGroup(grp); SessionContext sessionContext = new SessionContextImpl(session, leaderOfGroupsIds, memberOfGroupsIds, userRoles, factory .createStats(), roles, previous); return sessionContext; } public Session find(String uuid) { SessionContext sessionContext = cache.getSessionContext(uuid); checkIfShare(sessionContext); return (sessionContext == null) ? null : sessionContext.getSession(); } private void checkIfShare(SessionContext sessionContext) { if (sessionContext.getSession() instanceof Share) { final Long id = sessionContext.getSession().getId(); final String uuid = sessionContext.getSession().getUuid(); final String prefix = String.format("Share:%s (%s)", id, uuid); List<Object[]> rv = executeProjection( "select s.active, s.timeToLive, s.started from Share s where s.id = :id", new Parameters().addId(sessionContext.getSession().getId())); if (rv.size() != 1) { throw new RuntimeException(prefix + " could not be found!"); } Object[] items = rv.get(0); Boolean active = (Boolean) items[0]; Long timeToLive = (Long) items[1]; Timestamp started = (Timestamp) items[2]; if (Boolean.FALSE.equals(active)) { throw new SecurityViolation(prefix + " is inactive"); } else if ((System.currentTimeMillis() - started.getTime()) > timeToLive) { String msg = String.format("%s has expired: %s, timeToLive=%s", prefix, started, timeToLive); throw new SecurityViolation(msg); } } } private final static String findBy1 = "select s.id, s.uuid from Session s " + "join s.owner o where " + "s.closed is null and o.omeName = :name "; private final static String findByOrder = "order by s.started desc"; private List<Session> findByQuery(String query, Parameters p) { List<Object[]> ids_uuids = executeProjection(query, p); List<Session> rv = new ArrayList<Session>(); for (Object[] arr : ids_uuids) { String uuid = (String) arr[1]; try { SessionContext sc = cache.getSessionContext(uuid); rv.add(sc.getSession()); } catch (Exception e) { // skip } } return rv; } public List<Session> findByUser(String user) { return findByQuery(findBy1 + findByOrder, new Parameters().addString("name", user)); } public List<Session> findByUserAndAgent(String user, String... agents) { StringBuilder sb = new StringBuilder(); sb.append(findBy1); Parameters p = new Parameters().addString("name", user); if (agents == null || agents.length == 0 || (agents.length == 1 && agents[0] == null)) { sb.append("and s.userAgent is null "); } else { sb.append("and s.userAgent in (:agents) "); p.addList("agents", Arrays.asList(agents)); } sb.append(findByOrder); return findByQuery(sb.toString(), p); } public int getReferenceCount(String uuid) { SessionContext ctx = cache.getSessionContext(uuid); return ctx.count().get(); } public int detach(String uuid) { SessionContext ctx = cache.getSessionContext(uuid); return ctx.count().decrement(); } public SessionStats getSessionStats(String uuid) { SessionContext ctx = cache.getSessionContext(uuid); return ctx.stats(); } /* */ public int close(String uuid) { SessionContext ctx; try { ctx = cache.getSessionContext(uuid); } catch (SessionException se) { log.info("closeSession called but doesn't exist: " + uuid); return -1; // EARLY EXIT! } int refCount = ctx.count().decrement(); if (refCount < 1) { log.info("closeSession called and no more references: " + uuid); cache.removeSession(uuid); return -2; } else { log.info("closeSession called but " + refCount + " more references: " + uuid); return refCount; } } public Map<String, Map<String, Object>> getSessionData() { final Collection<String> ids = cache.getIds(); final Map<String, Map<String, Object>> rv = new HashMap<String, Map<String, Object>>(); for (String id : ids) { if (asroot.getName().equals(id)) { continue; // DON'T INCLUDE ROOT SESSION } try { rv.put(id, cache.getSessionData(id, true)); } catch (RemovedSessionException rse) { // Ok. Done for us } catch (SessionTimeoutException ste) { // Also ok } catch (Exception e) { log.warn(String.format("Exception thrown on getAll: %s:%s", e .getClass().getName(), e.getMessage())); } } return rv; } public int closeAll() { Collection<String> ids = cache.getIds(); for (String id : ids) { if (asroot.getName().equals(id)) { continue; // DON'T KILL OUR ROOT SESSION } try { log.info("closeAll called for " + id); cache.removeSession(id); } catch (RemovedSessionException rse) { // Ok. Done for us } catch (SessionTimeoutException ste) { // Also ok } catch (Exception e) { log.warn(String.format("Exception thrown on closeAll: %s:%s", e .getClass().getName(), e.getMessage())); } } return ids.size(); } public List<String> getUserRoles(String uuid) { SessionContext ctx = cache.getSessionContext(uuid); if (ctx == null) { throw new RemovedSessionException("No session with uuid: " + uuid); } return ctx.getUserRoles(); } // ~ State attached to session // ========================================================================= public Ehcache inMemoryCache(String uuid) { return cache.inMemoryCache(uuid); } public Ehcache onDiskCache(String uuid) { return cache.onDiskCache(uuid); } static String INPUT_ENVIRONMENT = "InputEnvironment"; static String OUTPUT_ENVIRONMENT = "OutputEnvironment"; public Object getInput(String session, String key) throws RemovedSessionException { return getEnvironmentVariable(session, key, INPUT_ENVIRONMENT); } public Object getOutput(String session, String key) throws RemovedSessionException { return getEnvironmentVariable(session, key, OUTPUT_ENVIRONMENT); } public Map<String, Object> inputEnvironment(String session) { return environment(session, INPUT_ENVIRONMENT); } public Map<String, Object> outputEnvironment(String session) { return environment(session, OUTPUT_ENVIRONMENT); } protected Map<String, Object> environment(String session, String env) { // Bump the last accessed time. // May throw a session exception getReferenceCount(session); Map<String, Object> rv = new HashMap<String, Object>(); Element elt = inMemoryCache(session).get(env); if (elt == null) { return rv; } @SuppressWarnings("unchecked") Map<String, Object> cv = (Map<String, Object>) elt.getObjectValue(); if (cv == null) { return rv; } rv.putAll(cv); return rv; } public void setInput(String session, String key, Object object) throws RemovedSessionException { setEnvironmentVariable(session, key, object, INPUT_ENVIRONMENT); } public void setOutput(String session, String key, Object object) throws RemovedSessionException { setEnvironmentVariable(session, key, object, OUTPUT_ENVIRONMENT); } private Object getEnvironmentVariable(String session, String key, String env) { Ehcache cache = inMemoryCache(session); Element elt = cache.get(env); if (elt == null) { return null; } @SuppressWarnings("unchecked") Map<String, Object> map = (Map<String, Object>) elt.getObjectValue(); if (map == null) { return null; } else { return map.get(key); } } @SuppressWarnings("unchecked") private void setEnvironmentVariable(String session, String key, Object object, String env) { Ehcache cache = inMemoryCache(session); Element elt = cache.get(env); Map<String, Object> map; if (elt == null) { map = new MapMaker().makeMap(); elt = new Element(env, map); cache.put(elt); } else { map = (Map<String, Object>) elt.getObjectValue(); } if (object == null) { map.remove(key); } else { map.put(key, object); } } // ~ Security methods // ========================================================================= public EventContext getEventContext(Principal principal) { final SessionContext ctx = cache.getSessionContext(principal.getName()); if (ctx == null) { throw new RemovedSessionException("No session with uuid:" + principal.getName()); } return ctx; } public EventContext reload(final String uuid) { final SessionContext ctx = cache.getSessionContext(uuid); if (ctx == null) { throw new RemovedSessionException("No session with uuid:" + uuid); } Future<Object> future = executor.submit(Priority.SYSTEM, new Callable<Object>(){ public Object call() throws Exception { cache.reload(uuid); return null; }}); // A freshly loaded session should now have been saved // as if it had been reloaded during synchronization. executor.get(future); return cache.getSessionContext(uuid); } // ~ Notifications // ========================================================================= public String[] notifications(String sessionId) { return null; } /** */ public void onApplicationEvent(ApplicationEvent event) { if (event instanceof UserGroupUpdateEvent) { cache.updateEvent((UserGroupUpdateEvent) event); } else if (event instanceof DestroySessionMessage) { executeCloseSession(((DestroySessionMessage) event).getSessionId()); } } // ~ Callbacks (Registering session-based components) // ========================================================================= public void addCallback(String sessionId, SessionCallback cb) { } public Object getCallbackObject(String sessionId, String name) { return null; } // ~ Misc // ========================================================================= /** * Checks the validity of the given {@link Principal}, and in the case of an * error attempts to correct the problem by returning a new Principal. */ private Principal validateSessionInputs( final ServiceFactory sf, final CreationRequest req) { final Principal p = req.principal; if (p == null || p.getName() == null) { throw new ApiUsageException("Null principal name."); } String type = p.getEventType(); if (StringUtils.isEmpty(type)) { type = "User"; } // Null or bad event type values as well as umasks are handled // within the SessionManager and EventHandler. String group = p.getGroup(); if (StringUtils.isEmpty(group)) { group = "user"; } // ticket:404 -- preventing users from logging into "user" group if (roles.getUserGroupName().equals(group)) { // Throws an exception if no properly defined default group ExperimenterGroup g = _getDefaultGroup(sf, p.getName()); if (g == null) { throw new ApiUsageException("Can't find default group for " + p.getName()); } group = g.getName(); } // Now we have a valid, non-"user" group, we can attempt to check // if the current context (e.g. group-sudo) is permitted to create // such a session. if (req.groupsLed != null) { long gid = sf.getAdminService().lookupGroup(group).getId(); if (!req.groupsLed.contains(gid)) { throw new SecurityViolation(String.format( "Group sudo is not permitted for group %s (gid=%s)", group, gid)); } } // Also checking event type. Throws if missing (and at least a NPE) type = sf.getTypesService().getEnumeration(EventType.class, type) .getValue(); Principal copy = new Principal(p.getName(), group, type); return copy; } private void parseAndSetDefaultType(String type, Session session) { String _type = (type == null) ? "User" : type; session.setDefaultEventType(_type); } /** * For the moment, user agent is nullable meaning that the only way to unset * a set value is by passing in null, so this is allowed here. This implies * that the best way to keep userAgent from being set to null is to always * return to ISession.updateSession() a session value which was originally * retrieved. */ private void parseAndSetUserAgent(String userAgent, Session session) { session.setUserAgent(userAgent); } private void parseAndSetTimeouts(Long timeToLive, Long timeToIdle, Session session, boolean trusted) { if (timeToLive != null) { if (trusted) { session.setTimeToLive(timeToLive); } else { // Let users set a value within reasons long activeTTL = Math.min(maxUserTimeToLive, timeToLive); // But if the value is 0, then the default must also be 0 if (activeTTL == 0 && defaultTimeToLive != 0) { throw new SecurityViolation("Cannot disable timeToLive. " + "Value must be between 1 and " + maxUserTimeToLive); } session.setTimeToLive(activeTTL); } } // As above if (timeToIdle != null) { if (trusted) { session.setTimeToIdle(timeToIdle); } else { long activeTTI = Math.min(maxUserTimeToIdle, timeToIdle); if (activeTTI == 0 && defaultTimeToIdle != 0) { throw new SecurityViolation("Cannot disable timeToIdle. " + "Value must be between 1 and " + maxUserTimeToIdle); } session.setTimeToIdle(activeTTI); } } } public Session copy(Session source) { if (source == null) { throw new ApiUsageException("Source may not be null."); } Session target; if (source instanceof Share) { target = newShare(); } else { target = new Session(); } target.setId(source.getId()); target.setClosed(source.getClosed()); target.setDefaultEventType(source.getDefaultEventType()); target.getDetails().shallowCopy(source.getDetails()); target.setMessage(source.getMessage()); target.setNode(source.getNode()); target.setStarted(source.getStarted()); target.setTimeToIdle(source.getTimeToIdle()); target.setTimeToLive(source.getTimeToLive()); target.setUserAgent(source.getUserAgent()); target.setUuid(source.getUuid()); if (target instanceof Share) { Share to = (Share) target; Share from = (Share) source; to.setItemCount(from.getItemCount()); to.setActive(from.getActive()); to.setGroup(from.getGroup()); to.setData(from.getData()); } return target; } // StaleCacheListener // ========================================================================= /** */ public void prepareReload() { // Noop } /** * Will be called in a synchronized block by {@link SessionCache} in order * to allow for an update. */ @SuppressWarnings({"rawtypes" }) public SessionContext reload(final SessionContext ctx) { final Principal p = new Principal(ctx.getCurrentUserName(), ctx .getCurrentGroupName(), ctx.getCurrentEventType()); List list = (List) executor.execute(asroot, new Executor.SimpleWork( this, "reload", ctx.getSession().getUuid()) { @Transactional(readOnly = true) public Object doWork(org.hibernate.Session session, ServiceFactory sf) { return executeSessionContextLookup(sf, p, ctx.getSession()); } }); if (list == null) { return null; } return createSessionContext(list, ctx); } // Executor methods // ========================================================================= @SuppressWarnings("unchecked") private List<Object[]> executeProjection(final String projection, final Parameters parameters) { return (List<Object[]>) executor.execute(asroot, new Executor.SimpleWork(this, "executeProjection", projection) { @Transactional(readOnly = true) public Object doWork(org.hibernate.Session session, ServiceFactory sf) { return sf.getQueryService().projection(projection, parameters); } }); } public boolean executePasswordCheck(final String name, final String credentials) { if (cache.getIds().contains(credentials)) { return true; } return executeCheckPassword(new Principal(name), credentials); } private Session executeUpdate(ServiceFactory sf, Session session, long userId) { Node node = sf.getQueryService().findByQuery( "select n from Node n where uuid = :uuid", new Parameters().addString("uuid", internal_uuid).setFilter( new Filter().page(0, 1))); if (node == null) { node = new Node(0L, false); // Using default node. } session.setNode(node); session.setOwner(new Experimenter(userId, false)); Session rv = sf.getUpdateService().saveAndReturnObject(session); rv.putAt("#2733", session.retrieve("#2733")); return rv; } private boolean executeCheckPassword(final Principal _principal, final String credentials) { Boolean ok = executeCheckPasswordRO(_principal, credentials); if (ok == null) { ok = executeCheckPasswordRW(_principal, credentials); } return ok; } private Boolean executeCheckPasswordRO(final Principal _principal, final String credentials) { return (Boolean) executor.execute(asroot, new Executor.SimpleWork(this, "executeCheckPasswordRO", _principal) { @Transactional(readOnly = true) public Object doWork(org.hibernate.Session session, ServiceFactory sf) { try { return ((LocalAdmin) sf.getAdminService()).checkPassword( _principal.getName(), credentials, true); } catch (Exception e) { // thrown if ldap is trying to create a user; // primarily a performance optimization to prevent // creating an event, etc. for all the password // checks which will *not* try to create a user. return null; } } }); } private Boolean executeCheckPasswordRW(final Principal _principal, final String credentials) { return (Boolean) executor.execute(asroot, new Executor.SimpleWork(this, "executeCheckPasswordRW", _principal) { @Transactional(readOnly = false) public Object doWork(org.hibernate.Session session, ServiceFactory sf) { return ((LocalAdmin) sf.getAdminService()).checkPassword( _principal.getName(), credentials, false); } }); } /** * Loads a session directly from the database, sets its "closed" value and * immediately saves it back to the database. This method is not called * directly from the {@link #close(String)} and {@link #closeAll()} methods * since there are other non-explicit ways for a session to be destroy, such * as a timeout within {@link SessionCache} and so this is called from * {@link #onApplicationEvent(ApplicationEvent)} when a * {@link DestroySessionMessage} is received. */ private Session executeCloseSession(final String uuid) { return (Session) executor .executeSql(new Executor.SimpleSqlWork(this, "executeCloseSession") { @Transactional(readOnly = false) public Object doWork(SqlAction sql) { try { int count = sql.closeSessions(uuid); if (count == 0) { log.warn("No session updated on closeSession:" + uuid); } else { log.debug("Session.closed set to now() for " + uuid); } } catch (Exception e) { log.error("FAILED TO CLOSE SESSION IN DATABASE: " + uuid, e); } return null; } }); } private Session executeInternalSession() { final Long sessionId = executeNextSessionId(); return (Session) executor .executeSql(new Executor.SimpleSqlWork(this, "executeInternalSession") { @Transactional(readOnly = false) public Object doWork(SqlAction sql) { // Create a basic session final Session s = new Session(); define(s, internal_uuid, "Session Manager internal", System.currentTimeMillis(), Long.MAX_VALUE, 0L, "Sessions", "Internal", null); // Set the owner and node specially for an internal sess long nodeId = 0L; try { nodeId = sql.nodeId(internal_uuid); } catch (EmptyResultDataAccessException erdae) { // Using default node } // SQL defined in data.vm for creating original session // (id,permissions,timetoidle,timetolive,started,closed,defaulteventtype,uuid,owner,node) // select nextval('seq_session'),-35, // 0,0,now(),now(),'rw----','PREVIOUSITEMS','1111',0,0; Map<String, Object> params = new HashMap<String, Object>(); params.put("sid", sessionId); params.put("ttl", s.getTimeToLive()); params.put("tti", s.getTimeToIdle()); params.put("start", s.getStarted()); params.put("type", s.getDefaultEventType()); params.put("uuid", s.getUuid()); params.put("node", nodeId); params.put("owner", roles.getRootId()); params.put("agent", s.getUserAgent()); params.put("ip", s.getUserIP()); int count = sql.insertSession(params); if (count == 0) { throw new InternalException( "Failed to insert new session: " + s.getUuid()); } Long id = sql.sessionId(s.getUuid()); s.setId(id); return s; } }); } /** * Added as an attempt to cure ticket:1176 * * @return */ private Long executeNextSessionId() { return (Long) executor .executeSql(new Executor.SimpleSqlWork(this, "executeNextSessionId") { @Transactional(readOnly = false) public Object doWork(SqlAction sql) { return sql.nextSessionId(); } }); } public ome.model.IObject setSecurityContext(Principal principal, ome.model.IObject obj) { final Long id = obj == null ? null : obj.getId(); if (id == null) { throw new ApiUsageException("Security context must be managed!"); } final SessionContext sc = cache.getSessionContext(principal.getName()); TextAnnotation ta = null; for (Annotation a : sc.getSession().linkedAnnotationList()) { if (a instanceof TextAnnotation) { if (roles.isRootUser(a.getDetails().getOwner())) { if (GROUP_SUDO_NS.equals(a.getNs())) { ta = (TextAnnotation) a; } } } if (ta != null) { String[] groupIds = ta.getTextValue().split(","); throw new SecurityViolation("Group-sudo session cannot change context!"); } } final long activeMethods = sc.stats().methodCount(); if (activeMethods != 0) { throw new SecurityViolation(activeMethods + " methods active. Aborting!"); } final Long shareId = sc.getCurrentShareId(); final Long groupId = sc.getCurrentGroupId(); ome.model.IObject prevCtx = null; if (shareId != null) { prevCtx = new Share(shareId, false); } else { prevCtx = new ExperimenterGroup(groupId, false); } ChangeSecurityContextEvent csce = new ChangeSecurityContextEvent( this, principal.getName(), prevCtx, obj); try { this.context.publishMessage(csce); csce.throwIfCancelled(); } catch (Throwable e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } else { InternalException ie = new InternalException("Failed to set call publishMessage"); ie.initCause(e); throw ie; } } if (obj instanceof ExperimenterGroup) { setGroupSecurityContext(principal, id); } else if (obj instanceof Share) { setShareSecurityContext(principal, id); } else { throw new ApiUsageException("Unknown security context:" + obj); } return prevCtx; } /** * * @see ticket:1434 */ private void setGroupSecurityContext(final Principal principal, final Long id) { final ome.system.EventContext ec = getEventContext(principal); final ExperimenterGroup[] group = new ExperimenterGroup[1]; final Session s = (Session) executor.execute(principal, new Executor.SimpleWork(this, "setGroupSecurityContext", id) { @Transactional(readOnly = true) public Object doWork(org.hibernate.Session session, ServiceFactory sf) { if (ec.getCurrentShareId() != null) { sf.getShareService().deactivate(); } SessionContext sc = cache.getSessionContext(principal.getName()); Session s = sc.getSession(); // Store old value for rollback if (!sc.isCurrentUserAdmin() && id >= 0 && !sc.getMemberOfGroupsList().contains(id)) { StringBuilder sb = new StringBuilder(); sb.append("User "); sb.append(sc.getCurrentUserId()); sb.append(" is not a member of group "); sb.append(id); throw new SecurityViolation(sb.toString()); } group[0] = s.getDetails().getGroup(); s.getDetails().setGroup(sf.getAdminService().getGroup(id)); return s; } }); // This could also be achieved by filtering out the "check group" // logic from BasicSecuritySystem. executor.execute(principal, new Executor.SimpleWork(this, "checkGroupSecurityContext", id) { @Transactional(readOnly = true) public Object doWork(org.hibernate.Session session, ServiceFactory sf) { // ticket:2088 - pre-emptive check try { sf.getAdminService().getEventContext(); } catch (RuntimeException re) { s.getDetails().setGroup(group[0]); throw re; } return null; } }); } /** * * @see ticket:1434 */ private void setShareSecurityContext(final Principal principal, final Long id) { executor.execute(principal, new Executor.SimpleWork(this, "setShareSecurityContext", id) { @Transactional(readOnly = true) public Object doWork(org.hibernate.Session session, ServiceFactory sf) { // ticket:2088 - ShareBean does the pre-emptive check sf.getShareService().activate(id); return null; } }); } // ~ Non-executor helpers // ========================================================================= /** * To prevent having the transaction rolled back, this method returns null * rather than throw an exception. */ private ExperimenterGroup _getDefaultGroup(ServiceFactory sf, String name) { LocalAdmin admin = (LocalAdmin) sf.getAdminService(); try { Experimenter exp = admin.userProxy(name); ExperimenterGroup grp = admin.getDefaultGroup(exp.getId()); return grp; } catch (Exception e) { log.warn("Exception while running " + "executeDefaultGroup", e); return null; } } /** * Looks up a user id by principal. If the name of the principal is actually * a removed user session, then a {@link RemovedSessionException} is thrown. */ private long executeLookupUser(ServiceFactory sf, Principal p) { List<Object[]> rv = sf.getQueryService().projection("select e.id from Experimenter e where e.omeName = :name", new Parameters().addString("name", p.getName())); if (rv.size() == 0) { throw new RemovedSessionException("Cannot find a user with name " + p.getName()); } return (Long) rv.get(0)[0]; } /** * Returns a List of state for creating a new {@link SessionContext}. If an * exception is thrown, return nulls since throwing an exception within the * Work will set our transaction to rollback only. */ private List<Object> executeSessionContextLookup(ServiceFactory sf, Principal principal, Session session) { try { List<Object> list = new ArrayList<Object>(); LocalAdmin admin = (LocalAdmin) sf.getAdminService(); final Experimenter exp = admin.userProxy(principal.getName()); final ExperimenterGroup grp = admin .groupProxy(principal.getGroup()); final List<Long> memberOfGroupsIds = admin.getMemberOfGroupIds(exp); final List<Long> leaderOfGroupsIds = admin.getLeaderOfGroupIds(exp); final List<String> userRoles = admin.getUserRoles(exp); final Session reloaded = (Session) sf.getQueryService().findByQuery( "select s from Session s " + "left outer join fetch s.annotationLinks l " + "left outer join fetch l.child a where s.id = :id", new Parameters().addId(session.getId())); list.add(exp); list.add(grp); list.add(memberOfGroupsIds); list.add(leaderOfGroupsIds); list.add(userRoles); list.add(principal); list.add(reloaded); return list; } catch (Exception e) { log.info("No info for " + principal.getName(), e); return null; } } private Share newShare() { Share share = new Share(); share.putAt("#2733", "ALLOW"); return share; } }