diff --git a/hawtio-web/src/main/java/io/hawt/system/Authenticator.java b/hawtio-web/src/main/java/io/hawt/system/Authenticator.java index fda364e8cd..8f2382b5a9 100644 --- a/hawtio-web/src/main/java/io/hawt/system/Authenticator.java +++ b/hawtio-web/src/main/java/io/hawt/system/Authenticator.java @@ -9,6 +9,7 @@ import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.AccountException; +import javax.security.auth.login.Configuration; import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; @@ -30,7 +31,7 @@ public class Authenticator { public static void extractAuthInfo(String authHeader, ExtractAuthInfoCallback cb) { authHeader = authHeader.trim(); - String [] parts = authHeader.split(" "); + String[] parts = authHeader.split(" "); if (parts.length != 2) { return; } @@ -48,26 +49,25 @@ public static void extractAuthInfo(String authHeader, ExtractAuthInfoCallback cb String password = parts[1]; cb.getAuthInfo(user, password); } - - } - public static AuthenticateResult authenticate(String realm, String role, String rolePrincipalClasses, HttpServletRequest request, PrivilegedCallback cb) { + public static AuthenticateResult authenticate(String realm, String role, String rolePrincipalClasses, Configuration configuration, + HttpServletRequest request, PrivilegedCallback cb) { String authHeader = request.getHeader(HEADER_AUTHORIZATION); if (authHeader == null || authHeader.equals("")) { return AuthenticateResult.NO_CREDENTIALS; } - + final AuthInfo info = new AuthInfo(); Authenticator.extractAuthInfo(authHeader, new ExtractAuthInfoCallback() { - @Override - public void getAuthInfo(String userName, String password) { - info.username = userName; - info.password = password; - } + @Override + public void getAuthInfo(String userName, String password) { + info.username = userName; + info.password = password; + } }); if (info.username == null || info.username.equals("public")) { @@ -75,8 +75,7 @@ public void getAuthInfo(String userName, String password) { } if (info.set()) { - - Subject subject = doAuthenticate(realm, role, rolePrincipalClasses, info.username, info.password); + Subject subject = doAuthenticate(realm, role, rolePrincipalClasses, configuration, info.username, info.password); if (subject == null) { return AuthenticateResult.NOT_AUTHORIZED; } @@ -95,32 +94,26 @@ public void getAuthInfo(String userName, String password) { return AuthenticateResult.NO_CREDENTIALS; } - private static Subject doAuthenticate(String realm, String role, String rolePrincipalClasses, final String username, final String password) { + private static Subject doAuthenticate(String realm, String role, String rolePrincipalClasses, Configuration configuration, + final String username, final String password) { try { if (LOG.isDebugEnabled()) { - LOG.debug("doAuthenticate[realm={}, role={}, rolePrincipalClasses={}, username={}, password={}]", new Object[]{realm, role, rolePrincipalClasses, username, "******"}); + LOG.debug("doAuthenticate[realm={}, role={}, rolePrincipalClasses={}, configuration={}, username={}, password={}]", + new Object[]{realm, role, rolePrincipalClasses, configuration, username, "******"}); } Subject subject = new Subject(); - LoginContext loginContext = new LoginContext(realm, subject, new CallbackHandler() { + CallbackHandler handler = new AuthenticationCallbackHandler(username, password); + + // call the constructor with or without the configuration as it behaves differently + LoginContext loginContext; + if (configuration != null) { + loginContext = new LoginContext(realm, subject, handler, configuration); + } else { + loginContext = new LoginContext(realm, subject, handler); + } - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (LOG.isTraceEnabled()) { - LOG.trace("Callback type {} -> {}", callback.getClass(), callback); - } - if (callback instanceof NameCallback) { - ((NameCallback)callback).setName(username); - } else if (callback instanceof PasswordCallback) { - ((PasswordCallback)callback).setPassword(password.toCharArray()); - } else { - LOG.warn("Unsupported callback class [" + callback.getClass().getName() + "]"); - } - } - } - }); loginContext.login(); if (role != null && role.length() > 0 && rolePrincipalClasses != null && rolePrincipalClasses.length() > 0) { @@ -155,9 +148,38 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback } catch (AccountException e) { LOG.warn("Account failure", e); } catch (LoginException e) { + // TODO: Add some option for verbosity logging + LOG.warn("Login failed", e); LOG.debug("Login failed", e); } return null; } + + private static final class AuthenticationCallbackHandler implements CallbackHandler { + + private final String username; + private final String password; + + private AuthenticationCallbackHandler(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (LOG.isTraceEnabled()) { + LOG.trace("Callback type {} -> {}", callback.getClass(), callback); + } + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(username); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(password.toCharArray()); + } else { + LOG.warn("Unsupported callback class [" + callback.getClass().getName() + "]"); + } + } + } + } } diff --git a/hawtio-web/src/main/java/io/hawt/web/AuthenticationConfiguration.java b/hawtio-web/src/main/java/io/hawt/web/AuthenticationConfiguration.java new file mode 100644 index 0000000000..287d44fe27 --- /dev/null +++ b/hawtio-web/src/main/java/io/hawt/web/AuthenticationConfiguration.java @@ -0,0 +1,63 @@ +package io.hawt.web; + +import javax.security.auth.login.Configuration; + +public class AuthenticationConfiguration { + + private boolean enabled; + private String realm; + private String role; + private String rolePrincipalClasses; + private Configuration configuration; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getRealm() { + return realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getRolePrincipalClasses() { + return rolePrincipalClasses; + } + + public void setRolePrincipalClasses(String rolePrincipalClasses) { + this.rolePrincipalClasses = rolePrincipalClasses; + } + + public Configuration getConfiguration() { + return configuration; + } + + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + @Override + public String toString() { + return "AuthenticationConfiguration[" + + "enabled=" + enabled + + ", realm='" + realm + '\'' + + ", role='" + role + '\'' + + ", rolePrincipalClasses='" + rolePrincipalClasses + '\'' + + ", configuration=" + configuration + + ']'; + } +} diff --git a/hawtio-web/src/main/java/io/hawt/web/AuthenticationFilter.java b/hawtio-web/src/main/java/io/hawt/web/AuthenticationFilter.java index fd0dc296dc..89b1883059 100644 --- a/hawtio-web/src/main/java/io/hawt/web/AuthenticationFilter.java +++ b/hawtio-web/src/main/java/io/hawt/web/AuthenticationFilter.java @@ -18,6 +18,7 @@ import io.hawt.system.ConfigManager; import io.hawt.system.Helpers; import io.hawt.system.PrivilegedCallback; +import io.hawt.web.tomcat.TomcatLoginContextConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,52 +35,57 @@ public class AuthenticationFilter implements Filter { public static final String HAWTIO_ROLE = "hawtio.role"; public static final String HAWTIO_ROLE_PRINCIPAL_CLASSES = "hawtio.rolePrincipalClasses"; - private String realm; - private String role; - private boolean enabled; - private String rolePrincipalClasses; + private final AuthenticationConfiguration configuration = new AuthenticationConfiguration(); @Override public void init(FilterConfig filterConfig) throws ServletException { - ConfigManager config = (ConfigManager) filterConfig.getServletContext().getAttribute("ConfigManager"); if (config != null) { - realm = config.get("realm", "karaf"); - role = config.get("role", "admin"); - rolePrincipalClasses = config.get("rolePrincipalClasses", ""); - enabled = Boolean.parseBoolean(config.get("authenticationEnabled", "true")); + configuration.setRealm(config.get("realm", "karaf")); + configuration.setRole(config.get("role", "admin")); + configuration.setRolePrincipalClasses(config.get("rolePrincipalClasses", "")); + configuration.setEnabled(Boolean.parseBoolean(config.get("authenticationEnabled", "true"))); } // JVM system properties can override always if (System.getProperty(HAWTIO_AUTHENTICATION_ENABLED) != null) { - enabled = Boolean.getBoolean(HAWTIO_AUTHENTICATION_ENABLED); + configuration.setEnabled(Boolean.getBoolean(HAWTIO_AUTHENTICATION_ENABLED)); } if (System.getProperty(HAWTIO_REALM) != null) { - realm = System.getProperty(HAWTIO_REALM); + configuration.setRealm(System.getProperty(HAWTIO_REALM)); } if (System.getProperty(HAWTIO_ROLE) != null) { - role = System.getProperty(HAWTIO_ROLE); + configuration.setRole(System.getProperty(HAWTIO_ROLE)); } if (System.getProperty(HAWTIO_ROLE_PRINCIPAL_CLASSES) != null) { - rolePrincipalClasses = System.getProperty(HAWTIO_ROLE_PRINCIPAL_CLASSES); + configuration.setRolePrincipalClasses(System.getProperty(HAWTIO_ROLE_PRINCIPAL_CLASSES)); + } + + // TODO: Introduce a discovery spi so we can try to figure out which runtime is in use, and auto-setup + // security accordingly, such as for Tomcat + + // or infer using tomcat as realm name, or have tomcat-user-database as the realm name as convention or something + // if we use tomcat as realm then use the tomcat principal class if not set + if ("tomcat".equals(configuration.getRealm()) && "".equals(configuration.getRolePrincipalClasses())) { + configuration.setRolePrincipalClasses("io.hawt.web.tomcat.TomcatPrincipal"); + configuration.setConfiguration(new TomcatLoginContextConfiguration()); } if (LOG.isDebugEnabled()) { - LOG.debug("Initializing AuthenticationFilter [enabled:{}, realm={}, role={}, rolePrincipalClasses={}]", new Object[]{enabled, realm, role, rolePrincipalClasses}); + LOG.debug("Initializing AuthenticationFilter {}", configuration); } - if (enabled) { - LOG.info("Starting hawtio authentication filter, JAAS realm: \"" + realm + "\" authorized role: \"" + role + "\"" + " role principal classes: \"" + rolePrincipalClasses + "\""); + if (configuration.isEnabled()) { + LOG.info("Starting hawtio authentication filter, JAAS realm: \"{}\" authorized role: \"{}\" role principal classes: \"{}\"", + new Object[]{configuration.getRealm(), configuration.getRole(), configuration.getRolePrincipalClasses()}); } else { LOG.info("Starting hawtio authentication filter, JAAS authentication disabled"); } - } @Override public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { - - if (realm == null || realm.equals("") || !enabled) { + if (configuration.getRealm() == null || configuration.getRealm().equals("") || !configuration.isEnabled()) { chain.doFilter(request, response); return; } @@ -105,7 +111,8 @@ public void doFilter(final ServletRequest request, final ServletResponse respons if (doAuthenticate) { LOG.debug("Doing authentication and authorization for path {}", path); - switch (Authenticator.authenticate(realm, role, rolePrincipalClasses, httpRequest, new PrivilegedCallback() { + switch (Authenticator.authenticate(configuration.getRealm(), configuration.getRole(), configuration.getRolePrincipalClasses(), + configuration.getConfiguration(), httpRequest, new PrivilegedCallback() { public void execute(Subject subject) throws Exception { executeAs(request, response, chain, subject); } diff --git a/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatLoginContextConfiguration.java b/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatLoginContextConfiguration.java new file mode 100644 index 0000000000..1df61b5361 --- /dev/null +++ b/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatLoginContextConfiguration.java @@ -0,0 +1,26 @@ +package io.hawt.web.tomcat; + +import java.util.HashMap; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +/** + * Configuration class to avoid having to deal with jaas.config files in the classpath + */ +public class TomcatLoginContextConfiguration extends Configuration { + + private final AppConfigurationEntry entry = new TomcatAppConfigurationEntry(); + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return new AppConfigurationEntry[]{entry}; + } + + private static final class TomcatAppConfigurationEntry extends AppConfigurationEntry { + + public TomcatAppConfigurationEntry() { + super("io.hawt.web.tomcat.TomcatUserDatabaseLoginContext", LoginModuleControlFlag.REQUIRED, new HashMap()); + } + } + +} diff --git a/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatPrincipal.java b/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatPrincipal.java new file mode 100644 index 0000000000..282bbc2fa4 --- /dev/null +++ b/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatPrincipal.java @@ -0,0 +1,20 @@ +package io.hawt.web.tomcat; + +import java.io.Serializable; +import java.security.Principal; + +public class TomcatPrincipal implements Principal, Serializable { + + // TODO: add role + + private final String name; + + public TomcatPrincipal(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } +} diff --git a/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatUserDatabaseLoginContext.java b/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatUserDatabaseLoginContext.java new file mode 100644 index 0000000000..34ad819b09 --- /dev/null +++ b/hawtio-web/src/main/java/io/hawt/web/tomcat/TomcatUserDatabaseLoginContext.java @@ -0,0 +1,97 @@ +package io.hawt.web.tomcat; + +import java.io.IOException; +import java.util.Map; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * To use Apache Tomcat's conf/tomcat-users.xml user database as JAAS {@link javax.security.auth.login.LoginContext}, + * so hawtio can use that for its {@link io.hawt.web.AuthenticationFilter}. + */ +public class TomcatUserDatabaseLoginContext implements LoginModule { + + private static final transient Logger LOG = LoggerFactory.getLogger(TomcatUserDatabaseLoginContext.class); + private Subject subject; + private CallbackHandler callbackHandler; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + } + + @Override + public boolean login() throws LoginException { + LOG.debug("Checking if user can login with Tomcat UserDatabase"); + + // get username and password + Callback[] callbacks = new Callback[2]; + callbacks[0] = new NameCallback("username"); + callbacks[1] = new PasswordCallback("password", false); + + try { + callbackHandler.handle(callbacks); + String username = ((NameCallback)callbacks[0]).getName(); + char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword(); + String password = new String(tmpPassword); + ((PasswordCallback)callbacks[1]).clearPassword(); + + // TODO: load conf/tomcat-users.xml file and check the username/role there + // TODO: or introduce a hawtio-tomcat module which uses catalina.jar API to + // lookup the UserDatabase in JNID if that would be possible + + // only allow login if password is secret + // as this is just for testing purpose + if (!"secret".equals(password)) { + throw new LoginException("Login denied"); + } + + // add roles + if ("scott".equals(username)) { + subject.getPrincipals().add(new TomcatPrincipal("admin")); + subject.getPrincipals().add(new TomcatPrincipal("guest")); + } else if ("guest".equals(username)) { + subject.getPrincipals().add(new TomcatPrincipal("guest")); + } + + } catch (IOException ioe) { + LoginException le = new LoginException(ioe.toString()); + le.initCause(ioe); + throw le; + } catch (UnsupportedCallbackException uce) { + LoginException le = new LoginException("Error: " + uce.getCallback().toString() + + " not available to gather authentication information from the user"); + le.initCause(uce); + throw le; + } + + return true; + } + + @Override + public boolean commit() throws LoginException { + return true; + } + + @Override + public boolean abort() throws LoginException { + return true; + } + + @Override + public boolean logout() throws LoginException { + subject = null; + callbackHandler = null; + return true; + } +}