package org.yamcs.security; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Hashtable; import java.util.concurrent.CompletableFuture; import org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.ConfigurationException; import org.yamcs.YConfiguration; import org.yamcs.api.artemis.YamcsSession; import org.yamcs.utils.YObjectLoader; import org.yamcs.xtce.MdbMappings; import org.yamcs.xtce.SequenceContainer; import org.yamcs.xtce.XtceDb; import org.yamcs.xtceproc.XtceDbFactory; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; /** * Implements privileges loaded and short-term cached from realm. * * Extending classes provide concrete method to identify the user, which this * class then uses to load privileges. This class also provides methods to * extract a username from a supplied certificate. * * Requests are usually made in short bursts, so privileges loaded from realm * are cached for a short period to reduce load on the realm service and retain * responsiveness. * * @author nm */ public class Privilege { public enum SystemPrivilege { MayControlProcessor, MayModifyCommandHistory, MayControlCommandQueue, MayCommand, MayGetMissionDatabase, MayControlArchiving, MayControlServices, MayReadTables, MayWriteTables, } private static String authModuleName; public static boolean usePrivileges = true; private static String defaultUser; // Only if !usePrivileges. Could eventually replace usePrivileges i guess private static AuthModule authModule; private static ActiveMQSecurityManager artemisAuthModule; static final Hashtable<String, String> contextEnv = new Hashtable<>(); public static int maxNoSessions; public static Privilege instance; static Logger log = LoggerFactory.getLogger(Privilege.class); public enum Type { SYSTEM, TC, TM_PACKET, TM_PARAMETER, TM_PARAMETER_SET } /** * Load configuration once only. */ static { defaultUser = "admin"; maxNoSessions = 10; usePrivileges = false; if (YConfiguration.isDefined("privileges")) { try { YConfiguration conf=YConfiguration.getConfiguration("privileges"); if (conf.containsKey("maxNoSessions")) { maxNoSessions=conf.getInt("maxNoSessions"); } usePrivileges=conf.getBoolean("enabled"); if(usePrivileges) { authModule = YObjectLoader.loadObject(conf.getMap("authModule")); authModuleName = authModule.getClass().getName(); if(conf.containsKey("artemisAuthModule")) { artemisAuthModule = YObjectLoader.loadObject(conf.getMap("artemisAuthModule")); } } else { if (conf.containsKey("defaultUser")) { String defaultUserString = conf.getString("defaultUser"); if (defaultUserString.isEmpty() || defaultUserString.contains(":")) { throw new ConfigurationException("Invalid name '" + defaultUserString + "' for default user"); } defaultUser = defaultUserString; } } } catch (IOException|ConfigurationException e) { throw new ConfigurationException("Failed to load 'privileges' configuration", e); } } if(usePrivileges) { log.info("Privileges enabled, authenticating and authorising by module {}", authModule); } else { log.warn("Privileges disabled, all connections are allowed and have full permissions"); } } public String[] getRoles(final AuthenticationToken authenticationToken) throws InvalidAuthenticationToken { if(!usePrivileges){ return null; } return authModule.getRoles(authenticationToken); } /** * loads the configuration of the privilege. If privileges.enabled is not * set to false in the privileges.properties, then load the privileges from * the LDAP. * * @throws ConfigurationException * when the privileges.enabled is not set to false and the ldap * parammeters are not present */ protected Privilege() throws ConfigurationException { } public static synchronized Privilege getInstance() { if (instance == null) instance = new Privilege() {}; return instance; } /** * * @return true if privileges are enabled */ public boolean isEnabled() { return usePrivileges; } /** * Convenience method, check if user has specific role. * @param authenticationToken * * @param role Title or full dn. * @return True if usePrivileges enabled and user has role, false otherwise. * @throws InvalidAuthenticationToken */ public boolean hasRole(final AuthenticationToken authenticationToken, String role ) throws InvalidAuthenticationToken { if(!usePrivileges) { return false; } if(authenticationToken == null || authenticationToken.getPrincipal() == null) { return false; } if (isSystemToken(authenticationToken)) { return true; } return authModule.hasRole(authenticationToken, role); } private boolean isSystemToken(final AuthenticationToken authenticationToken) { return authenticationToken.getPrincipal().equals(YamcsSession.hornetqInvmUser) || (authenticationToken instanceof SystemToken); } /** * * @param authenticationToken * @param type * @param privilege * a opsname of tc, tm parameter or tm packet * @return true if the privilege is known and the current user has it. * @throws InvalidAuthenticationToken */ public boolean hasPrivilege(final AuthenticationToken authenticationToken, Type type, String privilege) throws InvalidAuthenticationToken { if (!usePrivileges) { return true; } if(authenticationToken == null || authenticationToken.getPrincipal() == null) { return false; } if (isSystemToken(authenticationToken)) { return true; } return authModule.hasPrivilege(authenticationToken, type, privilege); } /** * Like the method above but instead of throwing InvalidAuthenticationToken, it returns false in case the token is not valid. * * To be used to avoid dealing with the exception in case we know that the token must be valid (i.e. immediately after a authenticate) * * @param authenticationToken * @param type * @param privilege * @return true if the user has the system privilege */ public boolean hasPrivilege1(final AuthenticationToken authenticationToken, Type type, String privilege) { try { return hasPrivilege(authenticationToken, type, privilege); } catch (InvalidAuthenticationToken e) { return false; } } /** * * @param authenticationToken * @param privilege * @return true if the user has the system privilege * @throws InvalidAuthenticationToken */ public boolean hasPrivilege(final AuthenticationToken authenticationToken, SystemPrivilege privilege) throws InvalidAuthenticationToken { if (!usePrivileges) return true; if(authenticationToken == null || authenticationToken.getPrincipal() == null) return false; if (isSystemToken(authenticationToken)) return true; return hasPrivilege(authenticationToken, Type.SYSTEM, privilege.name()); } /** * Like above but instead of throwing InvalidAuthenticationToken, it returns false if the token is not valid. * * @param authToken * @param sysPrivilege * @return true if the user has the system privilege */ public boolean hasPrivilege1(AuthenticationToken authToken, SystemPrivilege sysPrivilege) { try { return hasPrivilege(authToken, sysPrivilege); } catch (InvalidAuthenticationToken e) { return false; } } public static String getAuthModuleName() { return authModuleName; } /** * Returns the default user if this server is unsecured. Returns null in all other cases. * @return default username */ public static String getDefaultUser() { return defaultUser; } /** * Get packet names this user has appropriate privileges for. * * @param yamcsInstance Used to get MDB. * @param authToken * @param namespace If null defaults to "MDB:OPS Name" * @return A collection of TM packet names in the specified namespace for * which the user has privileges. * @throws ConfigurationException * @throws InvalidAuthenticationToken */ public Collection<String> getTmPacketNames(String yamcsInstance, final AuthenticationToken authToken, String namespace) throws ConfigurationException, InvalidAuthenticationToken { if( namespace == null ) { namespace = MdbMappings.MDB_OPSNAME; } Collection<String> tl= getTmPacketNames(XtceDbFactory.getInstance(yamcsInstance), namespace); ArrayList<String> l=new ArrayList<String>(); for(String name:tl) { if(!hasPrivilege(authToken, Privilege.Type.TM_PACKET, name)){ continue; } l.add(name); } return l; } private Collection<String> getTmPacketNames(XtceDb xtcedb, String namespace) { ArrayList<String> pn=new ArrayList<String>(); for(SequenceContainer sc:xtcedb.getSequenceContainers()){ String alias=sc.getAlias(namespace); if(alias!=null){ pn.add(alias); } } return pn; } /** * Get parameter names this user has appropriate privileges for. * * @param yamcsInstance Used to get MDB. * @param authToken * @param namespace If null defaults to "MDB:OPS Name" * @return A collection of TM parameter names in the specified namespace for * which the user has privileges. * @throws ConfigurationException * @throws InvalidAuthenticationToken */ public Collection<String> getTmParameterNames(String yamcsInstance, final AuthenticationToken authToken, String namespace) throws ConfigurationException, InvalidAuthenticationToken { if( namespace == null ) { namespace = MdbMappings.MDB_OPSNAME; } XtceDb xtcedb = XtceDbFactory.getInstance(yamcsInstance); ArrayList<String> l=new ArrayList<String>(); for(String name: xtcedb.getParameterNames() ) { if(!hasPrivilege(authToken, Privilege.Type.TM_PARAMETER, name)) { log.trace( "User '{}' does not have privilege '{}' for parameter '{}'", authToken, Privilege.Type.TM_PARAMETER, name ); continue; } l.add( xtcedb.getParameter( name ).getAlias( namespace ) ); } return l; } public int getMaxNoSessions() { return maxNoSessions; } public String getUsername(AuthenticationToken authToken) { if(!usePrivileges){ return defaultUser; } User u = authModule.getUser(authToken); if(u==null){ return null; } return u.getPrincipalName(); } public User getUser(AuthenticationToken authToken) { if(!usePrivileges) return null; return authModule.getUser(authToken); } public AuthModule getAuthModule() { return authModule; } public ActiveMQSecurityManager getArtemisAuthModule() { return artemisAuthModule; } public CompletableFuture<AuthenticationToken> authenticateHttp(ChannelHandlerContext ctx, HttpRequest req) { return authModule.authenticateHttp(ctx, req); } } class UsernameCache { UsernameCache(String un, long time) { username = un; lastUpdated = time; } String username; long lastUpdated; }