package org.yamcs.security; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.Interceptor; import org.apache.activemq.artemis.api.core.Message; import org.apache.activemq.artemis.core.protocol.core.Packet; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.CreateSessionMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.SessionCloseMessage; import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.SessionSendMessage; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Caches the username from an incoming CreateSessionMessage against its * connection, and injects the username into all subsequent Messages from the * connection until a SessionCloseMessage is sent by the client. * * When a CreateSessionMessage or a SessionCloseMessage is received, the cache * is also flushed to remove all connections which have a disconnected status. * * @author atu * */ public class ArtemisAuthInterceptor implements Interceptor { public static final String USERNAME_PROPERTY = "username"; static Logger log = LoggerFactory.getLogger("org.yamcs.security.ActiveMQAuthInterceptor"); private Map<Object, AuthInjectionAssociation> cache = Collections.synchronizedMap( new HashMap<Object, AuthInjectionAssociation>() ); @Override public boolean intercept(Packet packet, RemotingConnection connection) throws ActiveMQException { Object id = connection.getID(); AuthInjectionAssociation assoc = cache.get( id ); if( assoc != null ) { // Unregister if ( packet instanceof SessionCloseMessage ) { // Unregister connection flush( id ); flushDisconnected(); log.debug( "Total ### AuthInjectionAssociations {}", cache.size() ); } else { // Inject cached username if( packet instanceof SessionSendMessage ) { Message m = ((SessionSendMessage)packet).getMessage(); m.putStringProperty(USERNAME_PROPERTY, assoc.getUsername()); } } } else { if( packet instanceof CreateSessionMessage ) { // Register connection assoc = cache( ((CreateSessionMessage)packet).getUsername(), connection ); flushDisconnected(); log.debug( "Total ### AuthInjectionAssociations {}", cache.size() ); } } // Keep processing return true; } /** * Adds the user to the cache so all subsequent Messages have the username * injected. * * @param username * @param conn * @return */ public AuthInjectionAssociation cache( String username, RemotingConnection conn ) { AuthInjectionAssociation assoc = new AuthInjectionAssociation( username, conn ); cache.put( conn.getID(), assoc ); log.debug( "Start +++ {}", assoc ); return assoc; } /** * Stops username injection for the specified connection. * * Note a single client may make many connections. * * @param connectionId */ public void flush( Object connectionId ) { AuthInjectionAssociation assoc = cache.get(connectionId); log.debug( "Stop --- {}", assoc ); synchronized (cache) { cache.remove(connectionId); } } /** * Stops username injection for all connections made with the specified * username. * * Note a single username may be shared across many connections. * * @param username */ public void flush( String username ) { ArrayList<Object> ownedConnections = new ArrayList<Object>(); synchronized (cache) { for( Object connectionId : cache.keySet() ) { if( username.equals( cache.get( connectionId ).getUsername() ) ) { ownedConnections.add( connectionId ); } } } for( Object key : ownedConnections ) { flush( key ); } } /** * Stops username injection for all connections which have been destroyed. */ public void flushDisconnected() { ArrayList<Object> destroyedConnections = new ArrayList<Object>(); RemotingConnection conn = null; synchronized (cache) { for( Object connectionId : cache.keySet() ) { conn = cache.get(connectionId).getConnection(); if( conn != null && conn.isDestroyed() ) { destroyedConnections.add( connectionId ); } } } for( Object key : destroyedConnections ) { flush( key ); } } public Map<Object, AuthInjectionAssociation> getCache() { return cache; } } /** * Used by {@link ArtemisAuthInterceptor} to associate a connection with a username. * * @author atu * */ class AuthInjectionAssociation { private String username; private RemotingConnection connection; public AuthInjectionAssociation( String username, RemotingConnection connection ) { this.username = username; this.connection = connection; } public String getUsername() { return username; } public RemotingConnection getConnection() { return connection; } @Override public String toString() { return "AuthInjectionAssociation [connection "+connection.getID() +" (from "+connection.getRemoteAddress() +") with username '"+username+"']"; } }