Skip to content

Commit

Permalink
#857: Tomcat security support for its conf/tomcat-users.xml. Work in …
Browse files Browse the repository at this point in the history
…progress.
  • Loading branch information
davsclaus committed Dec 19, 2013
1 parent 2556c62 commit e1a7ce7
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 68 deletions.
@@ -0,0 +1,23 @@
package io.hawt.web;

/**
* SPI to allow various containers to discover and hook up needed configuration
* changes to {@link AuthenticationConfiguration} so hawtio {@link AuthenticationFilter}
* can integrate with the container easily.
*/
public interface AuthenticationContainerDiscovery {

/**
* Gets the container name such as Apache Tomcat, used for logging purpose
*/
String getContainerName();

/**
* Whether the container can/should be used for authentication
*
* @param configuration the configuration option (muteable)
* @return <tt>true</tt> if the container is being used for authentication.
*/
boolean canAuthenticate(AuthenticationConfiguration configuration);

}
39 changes: 20 additions & 19 deletions hawtio-web/src/main/java/io/hawt/web/AuthenticationFilter.java
Expand Up @@ -18,7 +18,7 @@
import io.hawt.system.ConfigManager;
import io.hawt.system.Helpers;
import io.hawt.system.PrivilegedCallback;
import io.hawt.web.tomcat.TomcatLoginContextConfiguration;
import io.hawt.web.tomcat.TomcatAuthenticationContainerDiscovery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -37,6 +37,11 @@ public class AuthenticationFilter implements Filter {

private final AuthenticationConfiguration configuration = new AuthenticationConfiguration();

// add known SPI authentication container discovery
private final AuthenticationContainerDiscovery[] discoveries = new AuthenticationContainerDiscovery[]{
new TomcatAuthenticationContainerDiscovery()
};

@Override
public void init(FilterConfig filterConfig) throws ServletException {
ConfigManager config = (ConfigManager) filterConfig.getServletContext().getAttribute("ConfigManager");
Expand All @@ -61,18 +66,13 @@ public void init(FilterConfig filterConfig) throws ServletException {
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 {}", configuration);
if (configuration.isEnabled()) {
for (AuthenticationContainerDiscovery discovery : discoveries) {
if (discovery.canAuthenticate(configuration)) {
LOG.info("Discovered container {} to use with hawtio authentication filter", discovery.getContainerName());
break;
}
}
}

if (configuration.isEnabled()) {
Expand All @@ -85,26 +85,26 @@ public void init(FilterConfig filterConfig) throws ServletException {

@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getServletPath();
LOG.debug("Handling request for path {}", path);

if (configuration.getRealm() == null || configuration.getRealm().equals("") || !configuration.isEnabled()) {
LOG.debug("No authentication needed for path {}", path);
chain.doFilter(request, response);
return;
}

HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpSession session = httpRequest.getSession(false);

LOG.debug("Handling request for path {}", httpRequest.getServletPath());

if (session != null) {
Subject subject = (Subject) session.getAttribute("subject");
if (subject != null) {
LOG.debug("Session subject {}", subject);
executeAs(request, response, chain, subject);
return;
}
}

String path = httpRequest.getServletPath();

boolean doAuthenticate = path.startsWith("/auth") ||
path.startsWith("/jolokia") ||
path.startsWith("/upload");
Expand All @@ -129,6 +129,7 @@ public void execute(Subject subject) throws Exception {
break;
}
} else {
LOG.debug("No authentication needed for path {}", path);
chain.doFilter(request, response);
}
}
Expand Down
@@ -0,0 +1,29 @@
package io.hawt.web.tomcat;

import io.hawt.web.AuthenticationConfiguration;
import io.hawt.web.AuthenticationContainerDiscovery;

/**
* To use Apache Tomcat using its conf/tomcat-users.xml for authentication.
*/
public class TomcatAuthenticationContainerDiscovery implements AuthenticationContainerDiscovery {

@Override
public String getContainerName() {
return "Apache Tomcat";
}

@Override
public boolean canAuthenticate(AuthenticationConfiguration configuration) {
// TODO: are we running in Tomcat
// lookup in JMX or env check?

boolean isTomcat = true;
if (isTomcat) {
configuration.setConfiguration(new TomcatLoginContextConfiguration());
configuration.setRolePrincipalClasses(TomcatPrincipal.class.getName());
}

return isTomcat;
}
}
Expand Up @@ -17,7 +17,6 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
}

private static final class TomcatAppConfigurationEntry extends AppConfigurationEntry {

public TomcatAppConfigurationEntry() {
super("io.hawt.web.tomcat.TomcatUserDatabaseLoginContext", LoginModuleControlFlag.REQUIRED, new HashMap<String, Object>());
}
Expand Down
@@ -1,6 +1,6 @@
package io.hawt.web.tomcat;

import java.io.IOException;
import java.io.File;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
Expand All @@ -10,9 +10,14 @@
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
* To use Apache Tomcat's conf/tomcat-users.xml user database as JAAS {@link javax.security.auth.login.LoginContext},
Expand All @@ -23,56 +28,59 @@ public class TomcatUserDatabaseLoginContext implements LoginModule {
private static final transient Logger LOG = LoggerFactory.getLogger(TomcatUserDatabaseLoginContext.class);
private Subject subject;
private CallbackHandler callbackHandler;
private String fileName = "conf/tomcat-users.xml";
private File file;

@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.file = new File(fileName);

if (!file.exists()) {
throw new IllegalStateException("Apache Tomcat user database file " + file + " does not exists");
}
}

@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 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");
((PasswordCallback) callbacks[1]).clearPassword();

LOG.debug("Getting user details for username {}", username);
String[] user = getUserPasswordRole(username);
if (user != null) {
if (!password.equals(user[1])) {
LOG.trace("Login denied due password did not match");
return false;
}
String[] roles = user[2].split(",");
for (String role : roles) {
LOG.trace("User {} has role {}", username, role);
subject.getPrincipals().add(new TomcatPrincipal(role));
}
} else {
LOG.trace("Login denied due user not found");
return false;
}

// 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;
} catch (Exception ioe) {
LoginException le = new LoginException(ioe.toString());
le.initCause(ioe);
throw le;
}

return true;
Expand All @@ -94,4 +102,24 @@ public boolean logout() throws LoginException {
callbackHandler = null;
return true;
}

protected String[] getUserPasswordRole(String username) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document dom = builder.parse(file);

NodeList users = dom.getElementsByTagName("user");
for (int i = 0; i < users.getLength(); i++) {
Node node = users.item(i);
String nUsername = node.getAttributes().getNamedItem("username").getNodeValue();
String nPassword = node.getAttributes().getNamedItem("password").getNodeValue();
String nRoles = node.getAttributes().getNamedItem("roles").getNodeValue();
if (username.equals(nUsername)) {
return new String[]{username, nPassword, nRoles};
}
}
return null;
}

}
22 changes: 2 additions & 20 deletions hawtio-web/src/main/webapp/WEB-INF/log4j.properties
@@ -1,22 +1,4 @@
#
# Copyright (C) FuseSource, Inc.
# http://fusesource.com
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

#
# The logging properties used during tests..
# logging configuration
#
log4j.rootLogger=INFO, console, file

Expand All @@ -30,5 +12,5 @@ log4j.appender.console.threshold=INFO
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d | %-5p | %m | %c | %t%n
log4j.appender.file.file=sample.log
log4j.appender.file.file=hawtio.log
log4j.appender.file.append=true

0 comments on commit e1a7ce7

Please sign in to comment.