/* * $Id$ * * Copyright 2006 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.security.basic; import java.util.ArrayList; import java.util.List; import ome.api.StatefulServiceInterface; import ome.conditions.ApiUsageException; import ome.conditions.InternalException; import ome.conditions.SessionTimeoutException; import ome.model.meta.Event; import ome.model.meta.EventLog; import ome.services.messages.ContextMessage; import ome.system.EventContext; import ome.tools.hibernate.SessionFactory; import ome.util.SqlAction; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.hibernate.Session; import org.springframework.context.ApplicationListener; import org.springframework.orm.hibernate3.HibernateTemplate; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttributeSource; /** * method interceptor responsible for login and creation of Events. Calls are * made to the {@link BasicSecuritySystem} provided in the * {@link EventHandler#EventHandler(SqlAction, BasicSecuritySystem, SessionFactory, TransactionAttributeSource, boolean) * constructor}. * * After the method is {@link MethodInterceptor#invoke(MethodInvocation) * invoked} various cleanup actions are performed and finally all credentials * all {@link BasicSecuritySystem#invalidateEventContext() cleared} from the * {@link Thread}. * * * * @author Josh Moore      <a * href="mailto:josh.moore@gmx.de">josh.moore@gmx.de</a> * @since 3.0 */ public class EventHandler implements MethodInterceptor, ApplicationListener<ContextMessage>{ private static Logger log = LoggerFactory.getLogger(EventHandler.class); protected final TransactionAttributeSource txSource; protected final BasicSecuritySystem secSys; protected final SessionFactory factory; protected final SqlAction sql; protected final boolean readOnly; /** * only public constructor, used for dependency injection. Requires an * active {@link HibernateTemplate} and {@link BasicSecuritySystem}. * * @param sql the SQL action * @param securitySystem the security system * @param factory the Hibernate session factory * @param txSource the Spring transaction attribute source */ public EventHandler(SqlAction sql, BasicSecuritySystem securitySystem, SessionFactory factory, TransactionAttributeSource txSource) { this(sql, securitySystem, factory, txSource, false); } public EventHandler(SqlAction sql, BasicSecuritySystem securitySystem, SessionFactory factory, TransactionAttributeSource txSource, boolean readOnly) { this.secSys = securitySystem; this.txSource = txSource; this.factory = factory; this.sql = sql; this.readOnly = readOnly; } /** * If a {@link ContextMessage} is received then we need to either add a * {@link ome.services.messages.ContextMessage.Push} login to the stack or * {@link ome.services.messages.ContextMessage.Pop} remove one. */ @Override public void onApplicationEvent(ContextMessage msg) { final CurrentDetails cd = secSys.cd; final Session session = factory.getSession(); if (msg instanceof ContextMessage.Pop){ secSys.disableReadFilter(session); // Disable old name cd.logout(); secSys.enableReadFilter(session); // With old context } else if (msg instanceof ContextMessage.Push) { // We assume don't close and use the current readOnly setting final EventContext curr = cd.getCurrentEventContext(); final boolean readOnly = curr.isReadOnly(); final boolean isClose = false; // here we try to reproduce what's done in invoke // with the addition of having a call context // ourselves secSys.disableReadFilter(session); // Disable old name cd.login(cd.getLast()); // Login with same principal cd.setContext(msg.context); if (!doLogin(readOnly, isClose)) { throw new InternalException("Failed to login on Push: " + msg.context); } secSys.enableReadFilter(session); // With new context } } /** * invocation interceptor for prepairing this {@link Thread} for execution * and subsequently reseting it. * * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) */ public Object invoke(MethodInvocation arg0) throws Throwable { boolean readOnly = checkReadOnly(arg0); boolean stateful = StatefulServiceInterface.class.isAssignableFrom(arg0 .getThis().getClass()); boolean isClose = stateful && "close".equals(arg0.getMethod().getName()); if (!readOnly && this.readOnly) { throw new ApiUsageException("This instance is read-only"); } // ticket:1254 // and ticket:1266 final Session session = factory.getSession(); if (!readOnly) { sql.deferConstraints(); } if (!doLogin(readOnly, isClose)) { return null; } boolean failure = false; Object retVal = null; try { secSys.enableReadFilter(session); retVal = arg0.proceed(); saveLogs(readOnly, session); secSys.cd.loadPermissions(session); return retVal; } catch (Throwable ex) { failure = true; throw ex; } finally { try { // on failure, we want to make sure that no one attempts // any further changes. if (failure) { // TODO we should probably do some forced clean up here. } // stateful services should NOT be flushed, because that's part // of the state that should hang around. else if (stateful) { // we don't want to do anything, really. } // read-only sessions should not have anything changed. else if (readOnly) { if (session.isDirty()) { if (log.isDebugEnabled()) { log.debug("Clearing dirty session."); } session.clear(); } } // stateless services, don't keep their sesssions about. else { session.flush(); if (session.isDirty()) { throw new InternalException( "Session is dirty. Cannot properly " + "reset security system. Must rollback.\n Session=" + session); } secSys.disableReadFilter(session); session.clear(); } } finally { secSys.disableReadFilter(session); secSys.invalidateEventContext(); } } } public boolean doLogin(boolean readOnly, boolean isClose) { try { secSys.loadEventContext(readOnly, isClose); } catch (SessionTimeoutException ste) { // If this is a CloseOnNoSessionContext then we skip all handling // since almost any action by the close() method will try to load // the context and will fail. This assumes that EventHandler is // the most inner handler. If this changes, then this logic may // need to be pushed down further. if (ste.sessionContext instanceof BasicSecurityWiring.CloseOnNoSessionContext) { log.debug("CloseOnNoSessionContext. Skipping"); return false; } throw ste; } // now the user can be considered to be logged in. EventContext ec = secSys.getEventContext(); if (!readOnly) { sql.prepareSession( ec.getCurrentEventId(), ec.getCurrentUserId(), ec.getCurrentGroupId()); } if (log.isInfoEnabled()) { StringBuilder sb = new StringBuilder(); sb.append(" Auth:\tuser="); sb.append(ec.getCurrentUserId()); sb.append(",group="); sb.append(ec.getCurrentGroupId()); sb.append(",event="); sb.append(ec.getCurrentEventId()); sb.append("("); sb.append(ec.getCurrentEventType()); sb.append("),sess="); sb.append(ec.getCurrentSessionUuid()); Long shareId = ec.getCurrentShareId(); if (shareId != null) { sb.append(",share="); sb.append(shareId); } log.info(sb.toString()); } return true; } /** * checks method (and as a fallback the class) for the Spring * {@link Transactional} annotation. * * @param mi * Non-null method invocation. * @return true if the {@link Transactional} annotation lists this method as * read-only, or if no annotation is found. */ boolean checkReadOnly(MethodInvocation mi) { TransactionAttribute ta = txSource.getTransactionAttribute(mi .getMethod(), mi.getThis().getClass()); return ta == null ? true : ta.isReadOnly(); } /** * Calling clearLogs posits that these EventLogs were successfully saved, * and so this method may raise an event signalling such. This could * eventually be reworked to be fully within the security system. */ void saveLogs(boolean readOnly, Session session) { // Grabbing a copy to prevent ConcurrentModificationEx final List<EventLog> logs = new ArrayList<EventLog>(secSys.getLogs()); secSys.clearLogs(); if (logs == null || logs.size() == 0) { return; // EARLY EXIT } if (readOnly) { // If we reach here, we have logs when we shouldn't. StringBuilder sb = new StringBuilder(); sb.append("EventLogs in readOnly transaction:\n"); for (EventLog eventLog : logs) { sb.append(eventLog.getAction()); sb.append(" "); sb.append(eventLog); sb.append(eventLog.getEntityType()); sb.append(" "); sb.append(eventLog.getEntityId()); sb.append("\b"); } throw new InternalException(sb.toString()); } try { long lastValue = sql.nextValue("seq_eventlog", logs.size()); long id = lastValue - logs.size() + 1; List<Object[]> batchData = new ArrayList<Object[]>(); for (EventLog l : logs) { Event e = l.getEvent(); if (e.getId() == null) { throw new RuntimeException("Transient event"); } batchData .add(new Object[] { id++, -35L, l.getEntityId(), l.getEntityType(), l.getAction(), l.getEvent().getId() }); } sql.insertLogs(batchData); } catch (Exception ex) { log.error("Error saving event logs: " + logs, ex); } if (secSys.getLogs().size() > 0) { throw new InternalException("More logs present after saveLogs()"); } } }