Mercurial > hg > release > thermostat-1.0
changeset 1087:105617e21576
Implement JAAS based authentication.
Reviewed-by: ebaron, neugens
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-May/006508.html
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distribution/config/thermostat-roles.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,42 @@ +# This file is used if the PropertiesUsernameRolesLoginModule is used +# as a delegate in the JAAS configuration *and* the 'roles.properties' option +# has not been specified for the login module. +# +# If that is the case, this file does two things: +# 1. It maps user names to roles. +# 2. Defines an optional recursive set of roles. This is useful in order to +# define role sets. Users can then be members of such defined role sets. +# Note that every line which does not have a user name (as defined in the +# corresponding users.properties file) on the left hand side of the +# equals sign ('='), represents a role. +# +# A user is assigned multiple roles by separating them by a comma ','. Every +# entity in this file which isn't a user name, will be implicitly defined as a +# role. +# +# Format is as follows: +# +# user1 = my-role, my-role2 +# user2 = new-role, role1 +# role1 = other-role +# +# Considering users 'user1' and 'user2' are defined in users.properties, the +# above would assign 'user1' the roles 'my-role' and 'my-role2'. 'user2' would +# be a member of 'new-role', 'role1' and 'other-role' (transitively via role1) +# +# +# +# Example recursive role definition allowed-to-do-everything agent-users. You +# can uncomment the following lines and assign your agent users this +# "thermostat-agent" role. +#thermostat-agent = thermostat-add, thermostat-replace, thermostat-update, \ +# thermostat-remove, thermostat-save-file, thermostat-purge, \ +# thermostat-register-category, thermostat-register-category, \ +# thermostat-cmdc-verify, thermostat-login, thermostat-realm +# +# Example recursive role definition for allowed-to-see-everything client-users. +# You may uncomment the following lines and assign your client users this +# "thermostat-client" role. +#thermostat-client = thermostat-realm, thermostat-login, thermostat-query, \ +# thermostat-cmdc-generate, thermostat-load-file, \ +# thermostat-get-count, thermostat-register-category
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distribution/config/thermostat-users.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,19 @@ +# This file is used if the PropertiesUsernameRolesLoginModule is used +# as a delegate in the JAAS configuration *and* the 'users.properties' option +# has not been specified for the login module. +# +# If that is the case, this defines the database of users with corresponding +# passwords, the thermostat web storage servlet knows about. +# +# WARNING: Passwords of users are in plain text. This needs to be considered +# when using this module in production. The main goal of this login +# module is to provide a simple way to define thermostat users and +# their corresponding passwords. +# +# The format of this file is as follows (whitespace in usernames/passwords are +# not recommended): +# +# user1=password1 +# user2=password2 +# ... +# \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/distribution/config/thermostat_jaas.conf Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,7 @@ +ThermostatJAASLogin { + com.redhat.thermostat.web.server.auth.spi.DelegateLoginModule required debug=true; +}; + +ThermostatJAASDelegate { + com.redhat.thermostat.web.server.auth.spi.PropertiesUsernameRolesLoginModule required debug=true; +}; \ No newline at end of file
--- a/distribution/pom.xml Tue May 14 18:09:15 2013 +0200 +++ b/distribution/pom.xml Fri May 03 19:14:14 2013 +0200 @@ -157,6 +157,9 @@ <filtering>true</filtering> <includes> <include>ssl.properties</include> + <include>thermostat-users.properties</include> + <include>thermostat-roles.properties</include> + <include>thermostat_jaas.conf</include> </includes> </resource> <resource>
--- a/integration-tests/pom.xml Tue May 14 18:09:15 2013 +0200 +++ b/integration-tests/pom.xml Fri May 03 19:14:14 2013 +0200 @@ -86,6 +86,7 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> + <argLine>-Djava.security.auth.login.config=${main.basedir}/distribution/target/etc/thermostat_jaas.conf ${coverageAgent}</argLine> <skip>true</skip> </configuration> <executions> @@ -192,6 +193,12 @@ </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-plus</artifactId> + <version>${jetty.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-webapp</artifactId> <version>${jetty.version}</version> <scope>test</scope>
--- a/integration-tests/src/test/java/com/redhat/thermostat/itest/IntegrationTest.java Tue May 14 18:09:15 2013 +0200 +++ b/integration-tests/src/test/java/com/redhat/thermostat/itest/IntegrationTest.java Fri May 03 19:14:14 2013 +0200 @@ -64,6 +64,10 @@ public static String getThermostatExecutable() { return "../distribution/target/bin/thermostat"; } + + public static String getThermostatHome() { + return "../distribution/target"; + } public static String getStorageDataDirectory() { return "../distribution/target/storage/db";
--- a/integration-tests/src/test/java/com/redhat/thermostat/itest/WebAppTest.java Tue May 14 18:09:15 2013 +0200 +++ b/integration-tests/src/test/java/com/redhat/thermostat/itest/WebAppTest.java Fri May 03 19:14:14 2013 +0200 @@ -39,18 +39,26 @@ import static com.redhat.thermostat.itest.IntegrationTest.assertNoExceptions; import static com.redhat.thermostat.itest.IntegrationTest.deleteFilesUnder; import static com.redhat.thermostat.itest.IntegrationTest.getStorageDataDirectory; +import static com.redhat.thermostat.itest.IntegrationTest.getThermostatHome; import static com.redhat.thermostat.itest.IntegrationTest.spawnThermostat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Properties; import java.util.UUID; +import javax.security.auth.login.LoginException; + import org.eclipse.jetty.security.DefaultUserIdentity; -import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.MappedLoginService; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.UserIdentity; @@ -58,6 +66,7 @@ import org.eclipse.jetty.webapp.WebAppContext; import org.junit.After; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -78,9 +87,15 @@ public class WebAppTest { + private static final String THERMOSTAT_USERS_FILE = getThermostatHome() + + "/etc/thermostat-users.properties"; + private static final String THERMOSTAT_ROLES_FILE = getThermostatHome() + + "/etc/thermostat-roles.properties"; private static WebStorage webStorage; private static Server server; private static int port; + private static Path backupUsers; + private static Path backupRoles; @BeforeClass public static void setUpOnce() throws Exception { @@ -92,8 +107,40 @@ storage.expectClose(); assertNoExceptions(storage.getCurrentStandardOutContents(), storage.getCurrentStandardErrContents()); + // Setup required for JAAS login module. FIXME: We should set this via + // a context listener. + System.setProperty("THERMOSTAT_HOME", getThermostatHome()); + backupUsers = Files.createTempFile("itest-backup-thermostat-users", ""); + backupRoles = Files.createTempFile("itest-backup-thermostat-roles", ""); + backupRoles.toFile().deleteOnExit(); + backupUsers.toFile().deleteOnExit(); + Files.copy(new File(THERMOSTAT_USERS_FILE).toPath(), backupUsers, StandardCopyOption.REPLACE_EXISTING); + Files.copy(new File(THERMOSTAT_ROLES_FILE).toPath(), backupRoles, StandardCopyOption.REPLACE_EXISTING); } + private void writeThermostatUsersRolesFile(Properties usersContent, Properties rolesContent) throws IOException { + File thermostatUsers = new File(THERMOSTAT_USERS_FILE); + File thermostatRoles = new File(THERMOSTAT_ROLES_FILE); + try (FileOutputStream usersStream = new FileOutputStream(thermostatUsers)) { + usersContent.store(usersStream, "integration-test users"); + } + try (FileOutputStream rolesStream = new FileOutputStream(thermostatRoles)) { + rolesContent.store(rolesStream, "integration-test roles"); + } + } + + @Before + public void setup() throws Exception { + // start the server, deploy the war + port = FreePortFinder.findFreePort(new TryPort() { + + @Override + public void tryPort(int port) throws Exception { + startServer(port); + } + }); + } + @After public void tearDown() throws Exception { server.stop(); @@ -108,16 +155,30 @@ storage.expectClose(); assertNoExceptions(storage.getCurrentStandardOutContents(), storage.getCurrentStandardErrContents()); + Files.copy(backupUsers, new File(THERMOSTAT_USERS_FILE).toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(backupRoles, new File(THERMOSTAT_ROLES_FILE).toPath(), StandardCopyOption.REPLACE_EXISTING); } - private static void startServer(int port, LoginService loginService) throws Exception { + private static void startServer(int port) throws Exception { server = new Server(port); ApplicationInfo appInfo = new ApplicationInfo(); String version = appInfo.getMavenVersion(); String warfile = "target/libs/thermostat-web-war-" + version + ".war"; WebAppContext ctx = new WebAppContext(warfile, "/thermostat"); - ctx.getSecurityHandler().setAuthMethod("BASIC"); - ctx.getSecurityHandler().setLoginService(loginService); + /* The web archive has a jetty-web.xml config file which sets up the + * JAAS config. If done in code, this would look like this: + * + * JAASLoginService loginS = new JAASLoginService(); + * loginS.setLoginModuleName("ThermostatJAASLogin"); + * loginS.setName("Thermostat Realm"); + * loginS.setRoleClassNames(new String[] { + * WrappedRolePrincipal.class.getName(), + * RolePrincipal.class.getName(), + * UserPrincipal.class.getName() + * }); + * ctx.getSecurityHandler().setLoginService(loginS); + * + */ server.setHandler(ctx); server.start(); } @@ -130,6 +191,20 @@ webStorage.getConnection().connect(); } + private void setupJAASForUser(String[] roleNames, String testuser, + String password) throws IOException { + Properties userProps = new Properties(); + userProps.put(testuser, password); + Properties roleProps = new Properties(); + StringBuffer roles = new StringBuffer(); + for (int i = 0; i < roleNames.length - 1; i++) { + roles.append(roleNames[i] + ", "); + } + roles.append(roleNames[roleNames.length - 1]); + roleProps.put(testuser, roles.toString()); + writeThermostatUsersRolesFile(userProps, roleProps); + } + @Test public void authorizedAdd() throws Exception { String[] roleNames = new String[] { @@ -140,14 +215,8 @@ }; String testuser = "testuser"; String password = "testpassword"; - final LoginService loginService = new TestLoginService(testuser, password, roleNames); - port = FreePortFinder.findFreePort(new TryPort() { - - @Override - public void tryPort(int port) throws Exception { - startServer(port, loginService); - } - }); + setupJAASForUser(roleNames, testuser, password); + connectStorage(testuser, password); webStorage.registerCategory(VmClassStatDAO.vmClassStatsCategory); @@ -160,7 +229,7 @@ add.setPojo(pojo); add.apply(); } - + @Test public void authorizedQuery() throws Exception { String[] roleNames = new String[] { @@ -171,14 +240,7 @@ }; String testuser = "testuser"; String password = "testpassword"; - final LoginService loginService = new TestLoginService(testuser, password, roleNames); - port = FreePortFinder.findFreePort(new TryPort() { - - @Override - public void tryPort(int port) throws Exception { - startServer(port, loginService); - } - }); + setupJAASForUser(roleNames, testuser, password); connectStorage(testuser, password); webStorage.registerCategory(VmClassStatDAO.vmClassStatsCategory); @@ -203,14 +265,7 @@ }; String testuser = "testuser"; String password = "testpassword"; - final LoginService loginService = new TestLoginService(testuser, password, roleNames); - port = FreePortFinder.findFreePort(new TryPort() { - - @Override - public void tryPort(int port) throws Exception { - startServer(port, loginService); - } - }); + setupJAASForUser(roleNames, testuser, password); connectStorage(testuser, password); byte[] data = "Hello World".getBytes();
--- a/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java Tue May 14 18:09:15 2013 +0200 +++ b/web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java Fri May 03 19:14:14 2013 +0200 @@ -340,7 +340,6 @@ public WebStorage(StartupConfiguration config) throws StorageException { ClientConnectionManager connManager = new ThreadSafeClientConnManager(); DefaultHttpClient client = new DefaultHttpClient(connManager); - client.getParams().setParameter("http.protocol.expect-continue", Boolean.TRUE); init(config, client, connManager); } @@ -579,7 +578,17 @@ InputStreamBody body = new InputStreamBody(in, name); MultipartEntity mpEntity = new MultipartEntity(); mpEntity.addPart("file", body); - post(endpoint + "/save-file", mpEntity).close(); + // See IcedTea bug #1314. For safe-file we need to do this. However, + // doing this for other actions messes up authentication when using + // jetty (and possibly others). Hence, do this expect-continue thingy + // only for save-file. + httpClient.getParams().setParameter("http.protocol.expect-continue", Boolean.TRUE); + try { + post(endpoint + "/save-file", mpEntity).close(); + } finally { + // FIXME: Not sure if we need this :/ + httpClient.getParams().removeParameter("http.protocol.expect-continue"); + } } @Override
--- a/web/server/pom.xml Tue May 14 18:09:15 2013 +0200 +++ b/web/server/pom.xml Fri May 03 19:14:14 2013 +0200 @@ -137,11 +137,9 @@ <Bundle-Vendor>Red Hat, Inc.</Bundle-Vendor> <Export-Package> com.redhat.thermostat.web.server, - com.redhat.thermostat.web.server.auth + com.redhat.thermostat.web.server.auth, + com.redhat.thermostat.web.server.auth.spi </Export-Package> - <Private-Package> - com.redhat.thermostat.web.server.internal - </Private-Package> <!-- Do not autogenerate uses clauses in Manifests --> <_nouses>true</_nouses> </instructions>
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java Tue May 14 18:09:15 2013 +0200 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java Fri May 03 19:14:14 2013 +0200 @@ -41,6 +41,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; +import java.security.Principal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -124,13 +125,20 @@ // service. The launcher did not run and hence THERMOSTAT_HOME is // not set and we need to do this ourselves. String thermostatHome = config.getInitParameter("THERMOSTAT_HOME"); + if (thermostatHome == null) { + String msg = "THERMOSTAT_HOME config parameter not set!"; + logger.log(Level.SEVERE, msg); + throw new RuntimeException(msg); + } File thermostatHomeFile = new File(thermostatHome); if (!thermostatHomeFile.canRead()) { // This is bad news. If we can't at least read THERMOSTAT_HOME // we are bound to fail in some weird ways at some later point. - throw new RuntimeException("THERMOSTAT_HOME = " + String msg = "THERMOSTAT_HOME = " + thermostatHome - + " is not readable or does not exist!"); + + " is not readable or does not exist!"; + logger.log(Level.SEVERE, msg); + throw new RuntimeException(msg); } logger.log(Level.INFO, "Setting THERMOSTAT_HOME for webapp to " + thermostatHome);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/BasicRole.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,90 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth; + +import java.io.Serializable; +import java.security.Principal; +import java.security.acl.Group; + +/** + * Base class for all thermostat roles. + * + * To be precise, every {@link Principal} which does NOT have the same name + * as the currently logged in user, are BasicRole principals. The name of a + * Principal is defined by {@link Principal#getName()}. + * + */ +public abstract class BasicRole implements Group, Serializable { + + private static final long serialVersionUID = -4572772782292794645L; + protected String name; + + public BasicRole(String name) { + this.name = name; + } + + /** + * Compares two BasicRoles. + * + * @return true if and only if other is a BasicRole and its name is the same + * as this role. + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof BasicRole)) { + return false; + } + String otherName = ((Principal) other).getName(); + boolean equals = false; + if (name == null) { + equals = otherName == null; + } else { + equals = name.equals(otherName); + } + return equals; + } + + @Override + public int hashCode() { + return (name == null ? 0 : name.hashCode()); + } + + @Override + public String getName() { + return name; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/RolePrincipal.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,122 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Vector; + +/** + * Class representing a simple thermostat role. Roles can be nested. + * + * @see Group + */ +public class RolePrincipal extends BasicRole { + + private static final long serialVersionUID = -7366668253791828610L; + // the set of nested roles if any + private final HashSet<Group> roles; + + /** + * Creates a role principal with the specified role name containing no roles + * within it. + * + * @param roleName + * @throws NullPointerException if roleName was null. + */ + public RolePrincipal(String roleName) { + super(Objects.requireNonNull(roleName)); + this.roles = new HashSet<>(); + } + + /** + * Adds a role this role principal. + * + * @return true if and only if the role has been successfully added. + * @throws IllegalArgumentException + * if the principal to be added is not a {@link Group} + * NullPointerException If the role was null. + */ + @Override + public boolean addMember(Principal role) { + if (!(Objects.requireNonNull(role) instanceof Group)) { + throw new IllegalArgumentException("principal not a group"); + } + return roles.add((Group) role); + } + + /** + * Removes a role from this role principal. + * + * @return true if the principal was successfully removed. + */ + @Override + public boolean removeMember(Principal role) { + // will return false if role not a member, omit check for Group + return roles.remove(role); + } + + @Override + public boolean isMember(Principal member) { + if (roles.contains(member)) { + return true; + } + // recursive case + Iterator<Group> it = roles.iterator(); + boolean isMember = false; + while (it.hasNext() && !isMember) { + Group group = it.next(); + isMember = isMember || group.isMember(member); + } + return isMember; + } + + @Override + public Enumeration<? extends Principal> members() { + Iterator<Group> it = roles.iterator(); + Vector<Principal> vector = new Vector<Principal>(); + while (it.hasNext()) { + vector.add(it.next()); + } + return vector.elements(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/UserPrincipal.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,119 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth; + +import java.io.Serializable; +import java.security.Principal; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Class representing thermostat users + * + */ +public class UserPrincipal implements Serializable, Principal { + + private static final Set<BasicRole> EMPTY_SET = new HashSet<>(0); + private static final long serialVersionUID = 2646753284881445421L; + // The set of roles this user is a member of (they may be nested) + private Set<BasicRole> roles; + // The name of this principal + private String name; + + /** + * Creates a new user principal with the given name + * + * @param name The user name. + * @throws NullPointerException if name is null. + */ + public UserPrincipal(String name) { + this.name = Objects.requireNonNull(name); + } + + /** + * + * @return The set of roles this principal is a member of. An empty set + * if this user has no role memberships. + */ + public Set<BasicRole> getRoles() { + if (roles == null) { + return EMPTY_SET; + } + return roles; + } + + /** + * Sets the set of roles which this principal is a member of. + * + * @param roles + * @throws NullPointerException If the given role set was null. + */ + public void setRoles(Set<BasicRole> roles) { + this.roles = Objects.requireNonNull(roles); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Principal)) { + return false; + } + String otherName = ((Principal) other).getName(); + boolean equals = false; + if (name == null) { + equals = otherName == null; + } else { + equals = name.equals(otherName); + } + return equals; + } + + @Override + public int hashCode() { + return (name == null ? 0 : name.hashCode()); + } + + @Override + public String toString() { + return this.getClass().getName() + ": " + name; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/WrappedRolePrincipal.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,48 @@ +package com.redhat.thermostat.web.server.auth; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Enumeration; + +/** + * Class representing a thermostat role. It simply wraps an existing {@link Group}. + * + * @see Group + */ +public class WrappedRolePrincipal extends BasicRole { + + private static final long serialVersionUID = -3852507889428067737L; + // the underlying group + private final Group group; + + /** + * Creates a role which delegates to the given group. + * + * @param group + */ + public WrappedRolePrincipal(Group group) { + super(group.getName()); + this.group = group; + } + + @Override + public boolean addMember(Principal user) { + return this.group.addMember(user); + } + + @Override + public boolean removeMember(Principal user) { + return this.group.removeMember(user); + } + + @Override + public boolean isMember(Principal member) { + return this.group.isMember(member); + } + + @Override + public Enumeration<? extends Principal> members() { + return this.group.members(); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/AbstractLoginModule.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,112 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +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 com.redhat.thermostat.common.utils.LoggingUtils; + +/** + * Base class for Thermostat JAAS login modules. + * + */ +public abstract class AbstractLoginModule implements LoginModule { + + private static final Logger logger = LoggingUtils.getLogger(AbstractLoginModule.class); + protected CallbackHandler callBackHandler; + + /** + * Get username and password from the callback. + * + * @return An array of length two where the first element is the username as + * String and the second element is the password as char[]. + * @throws LoginException + * if the retrieval fails (e.g. no callback is available). + */ + protected Object[] getUsernamePasswordFromCallBack() throws LoginException { + if (callBackHandler == null) { + throw new LoginException("No callback handler"); + } + Object[] creds = new Object[2]; + NameCallback nc = new NameCallback("Username: "); + PasswordCallback pc = new PasswordCallback("Password: ", false); + Callback[] callbacks = new Callback[] { nc, pc }; + try { + callBackHandler.handle(callbacks); + creds[0] = nc.getName(); + creds[1] = pc.getPassword(); + pc.clearPassword(); + return creds; + } catch (IOException | UnsupportedCallbackException e) { + logger.log(Level.FINEST, "Can't get username", e); + throw new LoginException(e.getMessage()); + } + } + + /** + * Get the user's name from the callback. + * + * @return The user's name. + * @throws LoginException + * if the retrieval fails (e.g. no callback is available). + */ + protected String getUsernameFromCallBack() throws LoginException { + if (callBackHandler == null) { + throw new LoginException("No callback handler"); + } + NameCallback nc = new NameCallback("Username: "); + PasswordCallback pc = new PasswordCallback("Password: ", false); + Callback[] callbacks = new Callback[] { nc, pc }; + try { + callBackHandler.handle(callbacks); + return nc.getName(); + } catch (IOException | UnsupportedCallbackException e) { + logger.log(Level.FINEST, "Can't get username", e); + throw new LoginException(e.getMessage()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/DelegateLoginModule.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,218 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import javax.servlet.http.HttpServletRequest; + +import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.web.server.auth.BasicRole; +import com.redhat.thermostat.web.server.auth.RolePrincipal; +import com.redhat.thermostat.web.server.auth.UserPrincipal; +import com.redhat.thermostat.web.server.auth.WrappedRolePrincipal; + +/** + * LoginModule which delegates to the configured implementing + * {@link LoginModule}. This is useful in order to be able to provide a + * guarrantee that a {@link UserPrincipal} is always returned for + * {@link HttpServletRequest#getUserPrincipal()} if any. + * + */ +public final class DelegateLoginModule extends AbstractLoginModule { + + private static final Logger logger = LoggingUtils.getLogger(DelegateLoginModule.class); + private static final String JAAS_DELEGATE_CONFIG_NAME = "ThermostatJAASDelegate"; + // the delegate + private LoginContext delegateContext; + private Subject subject; + private String username; + /** + * The config name to use. Defaults to {@linkplain DelegateLoginModule#JAAS_DELEGATE_CONFIG_NAME} + */ + private String configName; + + /** + * Default, no-arg constructor. + */ + public DelegateLoginModule() { + this.configName = JAAS_DELEGATE_CONFIG_NAME; + } + + // used for testing + DelegateLoginModule(String configName) { + this.configName = configName; + } + + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map<String, ?> sharedState, Map<String, ?> options) { + this.subject = subject; + this.callBackHandler = callbackHandler; + /* + * Create and initialize the delegate + */ + try { + this.delegateContext = new LoginContext(configName, subject, callbackHandler); + logger.log(Level.FINEST, "successfully created delegate login context"); + } catch (LoginException e) { + // This only happens if there is no "ThermostatJAASDelegate" config + // and also no configuration with the name "other", which is likely + // always there for real application servers. + String message = "Could not initialize delegate. " + + "'ThermostatJAASDelegate' " + + "and 'other' login modules are both not configured!"; + logger.log(Level.SEVERE, message, e); + throw new RuntimeException(message); + } + } + + @Override + public boolean login() throws LoginException { + boolean loginOk = false; + try { + username = super.getUsernameFromCallBack(); + logger.log(Level.FINEST, "Attempt to login as " + username + "(using delegate)"); + delegateContext.login(); + loginOk = true; + logger.log(Level.FINEST, "Login succeeded"); + } catch (LoginException e) { + // This has a level of fine since it's just a plain login failure + logger.log(Level.FINEST, "Login failed", e); + throw e; + } + return loginOk; + } + + @Override + public boolean commit() throws LoginException { + /* + * Make sure to retrieve principals from the authenticated subject, + * wrap them in UserPrincipal/BasicRole principals and inform the + * UserPrincipal about the roles it is a member of. + */ + Set<Principal> principals = subject.getPrincipals(); + int size = principals.size(); + Set<Principal> wrappedPrincipals = new HashSet<>(size); + // the user principal is not in the roles set + Set<BasicRole> roles = new HashSet<>(size -1); + Iterator<Principal> it = principals.iterator(); + UserPrincipal userPrincipal = null; + while (it.hasNext()) { + Principal p = it.next(); + if (p.getName().equals(username)) { + // add our user principal + if (userPrincipal != null) { + logger.log(Level.SEVERE, "> 1 user principals!"); + throw new IllegalStateException("> 1 user principals!"); + } + userPrincipal = new UserPrincipal(username); + wrappedPrincipals.add(userPrincipal); + } else { + // group (a.k.a role). It may be a simple principal or a + // Group. If it is a group, we simply wrap it. If it isn't + // we use our simple RolePrincipal instead. + BasicRole role; + if (p instanceof Group) { + role = new WrappedRolePrincipal((Group)p); + wrappedPrincipals.add(role); + roles.add(role); + } else { + role = new RolePrincipal(p.getName()); + wrappedPrincipals.add(role); + roles.add(role); + } + } + } + // Remove old principals and push the newly wrapped ones + principals.clear(); + principals.addAll(wrappedPrincipals); + // Finally, inform the user principal about the roles it is a member of. + // We need this in order to be able to do something (filtering/authorization) + // with these roles from the web storage servlet. + userPrincipal.setRoles(roles); + + logger.log(Level.FINEST, "Committed changes for '" + username + "'"); + return true; + } + + @Override + public boolean abort() throws LoginException { + if (subject != null) { + // remove any principals + Set<Principal> principals = subject.getPrincipals(); + principals.clear(); + } + logger.log(Level.FINEST, "Aborted login!"); + return true; + } + + @Override + public boolean logout() throws LoginException { + try { + delegateContext.logout(); + Set<Principal> principals = subject.getPrincipals(); + principals.clear(); + logger.log(Level.FINEST, "Logged out"); + return true; + } catch (LoginException e) { + logger.log(Level.FINEST, "Logout failed!", e); + return false; + } + } + + /* + * Package private method in order to get at the subject + */ + final Subject getSubject() { + return this.subject; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUserValidator.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,127 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.redhat.thermostat.common.config.Configuration; +import com.redhat.thermostat.common.config.InvalidConfigurationException; +import com.redhat.thermostat.common.utils.LoggingUtils; + +/** + * + * Validates users against an internal, properties based, user database. + * + */ +class PropertiesUserValidator implements UserValidator { + + private static final Logger logger = LoggingUtils.getLogger(PropertiesUserValidator.class); + private static final String DEFAULT_USERS_FILE = "thermostat-users.properties"; + private Properties users; + + PropertiesUserValidator() { + this((new Configuration().getConfigurationDir() + File.separator + DEFAULT_USERS_FILE)); + } + + PropertiesUserValidator(String usersFile) { + loadUsers(new File(usersFile)); + } + + @Override + public synchronized void authenticate(String username, char[] password) + throws UserValidationException { + if (users == null) { + throw new UserValidationException("No user database"); + } + String tmp = users.getProperty(username); + if (tmp == null) { + throw new UserValidationException("User '" + username + "' not found"); + } + // We have an entry in our user db for the requested username. + char refPassWd[] = tmp.toCharArray(); + tmp = null; + try { + validate(password, refPassWd); + } finally { + // clear our password + for (int i = 0; i < refPassWd.length; i++) { + refPassWd[i] = '\0'; + } + } + } + + private void validate(char[] theirPwd, char[] ourPwd) throws UserValidationException { + String failureMessage = "Login failed!"; + if (theirPwd.length != ourPwd.length) { + throw new UserValidationException(failureMessage); + } + for (int i = 0; i < theirPwd.length; i++) { + if (theirPwd[i] != ourPwd[i]) { + throw new UserValidationException(failureMessage); + } + } + } + + private void loadUsers(File userDB) { + if (users == null) { + Properties users = new Properties(); + try (FileInputStream stream = new FileInputStream(userDB)) { + users.load(stream); + this.users = users; + } catch (IOException e) { + String msg = "Unable to load user database"; + logger.log(Level.WARNING, msg, e); + throw new InvalidConfigurationException(msg); + } + } + } + + @Override + public Set<Object> getAllKnownUsers() throws IllegalStateException { + if (users == null) { + throw new IllegalStateException("No user database"); + } + return Collections.unmodifiableSet(users.keySet()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUsernameRolesLoginModule.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,214 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; + +import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.web.server.auth.BasicRole; +import com.redhat.thermostat.web.server.auth.RolePrincipal; +import com.redhat.thermostat.web.server.auth.UserPrincipal; + +/** + * <p> + * A login module which uses properties files for validating users and for + * amending authenticated users with appropriate roles. + * </p> + * <p> + * The properties file which will be used for user credential validation can be + * specified via the <code>users.properties</code> option and defaults to + * <code>$THERMOSTAT_HOME/thermostat-users.properties</code> if no such option + * has been specified. Mappings for users to roles come from a file as specified + * by the <code>roles.properties</code> option and defaults to + * <code>$THERMOSTAT_HOME/thermostat-roles.properties</code> if no such option + * has been provided. + * </p> + */ +public class PropertiesUsernameRolesLoginModule extends AbstractLoginModule { + + private static Logger logger = LoggingUtils.getLogger(PropertiesUsernameRolesLoginModule.class); + + private Subject subject; + // The validator to use for authentication + private UserValidator validator; + private RolesAmender amender; + private String username; + private boolean loginOK = false; + + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map<String, ?> sharedState, Map<String, ?> options) { + this.subject = subject; + this.callBackHandler = callbackHandler; + this.validator = getValidator((String) options.get("users.properties")); + this.amender = getRolesAmender((String) options.get("roles.properties"), + validator.getAllKnownUsers()); + } + + @Override + public boolean login() throws LoginException { + logger.log(Level.FINEST, "Logging in"); + loginOK = false; + char[] password = null; + try { + Object[] creds = super.getUsernamePasswordFromCallBack(); + username = (String)creds[0]; + password = (char[])creds[1]; + validator.authenticate(username, password); + logger.log(Level.FINEST, "Logged in user '" + username + "'"); + loginOK = true; + } catch (UserValidationException e) { + logger.log(Level.FINE, "Authentication failed for user '" + username + "'"); + throw new LoginException(e.getMessage()); + } finally { + clearPassword(password); + } + return loginOK; + } + + @Override + public boolean commit() throws LoginException { + if (loginOK == false) { + return false; + } + logger.log(Level.FINEST, "Committing principals for user '" + username + "'"); + Set<Principal> principals = subject.getPrincipals(); + // Tomcat uses classes as specified by the LoginModule config + // in order to distinguish between user principals and role principals + // JBoss on the other hand uses string based name matching for the + // user principal + principals.add(new UserPrincipal(username)); + Set<BasicRole> roles = null; + try { + roles = amender.getRoles(username); + } catch (IllegalStateException e) { + logger.log(Level.FINE, "Failed to commit", e); + throw new LoginException(); + } + principals.addAll(roles); + // JBoss uses the Group "Roles" as the principal + // for role matching + Group rolesRole = new RolePrincipal("Roles"); + Iterator<BasicRole> it = roles.iterator(); + while (it.hasNext()) { + BasicRole r = it.next(); + rolesRole.addMember(r); + } + principals.add(rolesRole); + return true; + } + + @Override + public boolean abort() throws LoginException { + logger.log(Level.FINEST, "aborting"); + clearPrincipals(); + return true; + } + + @Override + public boolean logout() throws LoginException { + logger.log(Level.FINEST, "logging out"); + clearPrincipals(); + return true; + } + + private void clearPassword(char[] password) { + if (password == null) { + return; + } + for (int i= 0; i < password.length; i++) { + password[i] = '\0'; + } + } + + private void clearPrincipals() { + Set<Principal> principals = subject.getPrincipals(); + principals.clear(); + } + + private UserValidator getValidator(final String usersProperties) { + UserValidator validator = null; + try { + if (usersProperties == null) { + validator = new PropertiesUserValidator(); + logger.log(Level.FINE, "Using default user database"); + } else { + logger.log(Level.FINE, "Using user database as defined in file '" + usersProperties + "'"); + validator = new PropertiesUserValidator(usersProperties); + } + } catch (Throwable e) { + // Can't continue at this point, since we this for + // authentication. + String msg = "Failed to initialize user database"; + logger.log(Level.SEVERE, msg, e); + throw new RuntimeException(msg); + } + return validator; + } + + private RolesAmender getRolesAmender(final String rolesProperties, final Set<Object> users) { + RolesAmender roleAmender = null; + try { + if (rolesProperties == null) { + roleAmender = new RolesAmender(users); + logger.log(Level.FINE, "Using default roles database"); + } else { + logger.log(Level.FINE, "Using roles database as defined in file '" + rolesProperties + "'"); + roleAmender = new RolesAmender(rolesProperties, users); + } + } catch (Throwable e) { + // Can't continue at this point, since we this for + // authentication. + String msg = "Failed to initialize role/user mapping database"; + logger.log(Level.SEVERE, msg, e); + throw new RuntimeException(msg, e); + } + return roleAmender; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/RolesAmender.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,235 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ +package com.redhat.thermostat.web.server.auth.spi; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.redhat.thermostat.common.config.Configuration; +import com.redhat.thermostat.common.config.InvalidConfigurationException; +import com.redhat.thermostat.common.utils.LoggingUtils; +import com.redhat.thermostat.web.server.auth.BasicRole; +import com.redhat.thermostat.web.server.auth.RolePrincipal; + +/** + * Class responsible for parsing roles from a properties file. + * + */ +class RolesAmender { + + private static final Logger logger = LoggingUtils.getLogger(RolesAmender.class); + private static final String DEFAULT_ROLES_FILE = "thermostat-roles.properties"; + private static final String ROLE_SEPARATOR = ","; + private static final Set<BasicRole> EMPTY_SET = new HashSet<>(0); + // A username => roles mapping + private Map<String, Set<BasicRole>> rolesMap; + // The set of all users we know about + private final Set<Object> users; + + RolesAmender(Set<Object> users) { + this((new Configuration().getConfigurationDir() + File.separator + DEFAULT_ROLES_FILE), users); + } + + RolesAmender(String rolesFile, Set<Object> users) { + this.users = Objects.requireNonNull(users); + loadRoles(new File(rolesFile)); + } + + /** + * Gets the set of roles for a specific username. + * + * @param username + * @return The role-set for the given user or an empty set if this user is + * not a member of any role. + * @throws IllegalStateException If the roles database was null. + */ + Set<BasicRole> getRoles(String username) throws IllegalStateException { + if (rolesMap == null) { + throw new IllegalStateException("Roles database missing"); + } + Set<BasicRole> roles = rolesMap.get(username); + if (roles == null) { + // username not in list of roles, default to empty set for her + return EMPTY_SET; + } + return roles; + } + + private void loadRoles(File file) { + if (rolesMap == null) { + Properties rawRoles = new Properties(); + try (FileInputStream stream = new FileInputStream(file)) { + rawRoles.load(stream); + } catch (IOException e) { + String msg = "Failed to load roles from properties"; + logger.log(Level.SEVERE, msg, e); + throw new InvalidConfigurationException(msg); + } + try { + prepareRolesMap(rawRoles); + } catch (Throwable e) { + String msg = "Failed to parse roles"; + logger.log(Level.SEVERE, msg, e); + throw new IllegalStateException(msg); + } + } + } + + private void prepareRolesMap(Properties rawRoles) { + rolesMap = new HashMap<>(); + Map<String, RolesInfo> rolesSoFar = new HashMap<>(); + Iterator<Object> it = users.iterator(); + while (it.hasNext()) { + // users came from props, this should be a safe cast to string + String username = (String)it.next(); + String rolesRaw = null; + rolesRaw = rawRoles.getProperty(username); + if (rolesRaw == null) { + // Since the list of usernames is not necessarily a subset of + // lines listed in roles for the case where there are just users + // defined, but don't appear in the roles file (i.e. have no + // role memberships). In this case, we simply skip this entry. + continue; + } + rolesRaw = rolesRaw.trim(); + String[] roles = rolesRaw.split(ROLE_SEPARATOR); + Set<BasicRole> uRoles = new HashSet<>(); + for (String tmp: roles) { + String role = tmp.trim(); + if (role.equals("")) { + // skip empty role names + continue; + } + RolesInfo info = rolesSoFar.get(role); + if (info == null) { + // new role, define it and create role-info + BasicRole r = new RolePrincipal(role); + info = new RolesInfo(r); + info.getMemberUsers().add(username); + rolesSoFar.put(role, info); + } + info.getMemberUsers().add(username); + uRoles.add(info.getRole()); + } + // finished one username, add it to roles map, to the intermediate + // set cache, and remove entry from raw roles list. + rolesMap.put(username, uRoles); + rawRoles.remove(username); + } + // what's left now are recursive role definitions. + Set<Object> recursiveRoles = rawRoles.keySet(); + for (Object r: recursiveRoles) { + RolesInfo recRole = rolesSoFar.get((String)r); + if (recRole == null) { + // This is bad news, new role defined but no username we know + // of is member of that role + throw new IllegalStateException("Recursive role '" + r + "' defined, but no user is a member"); + } + String memberRoles = rawRoles.getProperty((String)r).trim(); + for (String tmp: memberRoles.split(ROLE_SEPARATOR)) { + String member = tmp.trim(); + if (member.equals("")) { + // skip empty role name + continue; + } + if (users.contains(member)) { + throw new IllegalStateException("User '" + member + "' part of recursive role definition!"); + } + RolesInfo role = rolesSoFar.get(member); + if (role == null) { + // new role, define it and add it as member to recursive + // role + BasicRole ro = new RolePrincipal(member); + role = new RolesInfo(ro); + rolesSoFar.put(member, role); + } + recRole.getRole().addMember(role.getRole()); + } + expandRoles(recRole); + } + } + + /* + * Add roles to users which are member of a recursive role + */ + private void expandRoles(RolesInfo recRole) { + @SuppressWarnings("unchecked") // we've added them so safe + Enumeration<BasicRole> members = (Enumeration<BasicRole>)recRole.getRole().members(); + Iterator<String> memberUsersIterator = recRole.getMemberUsers().iterator(); + while (memberUsersIterator.hasNext()) { + String username = memberUsersIterator.next(); + Set<BasicRole> userRoles = rolesMap.get(username); + while (members.hasMoreElements()) { + BasicRole r = members.nextElement(); + userRoles.add(r); + } + } + } + + /* + * Container data structure which is used for (role => member users) lookup. + */ + static class RolesInfo { + + private final Set<String> memberUsers; + private final BasicRole role; + + RolesInfo(BasicRole role) { + this.role = role; + memberUsers = new HashSet<>(); + } + + Set<String> getMemberUsers() { + return memberUsers; + } + + BasicRole getRole() { + return role; + } + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/UserValidationException.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,51 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +/** + * Exception thrown if user credential validation failed. + * + * @see UserValidator + */ +public class UserValidationException extends Exception { + + private static final long serialVersionUID = -4552159492521496288L; + + public UserValidationException(String reason) { + super(reason); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/UserValidator.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,70 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.util.Set; + +/** + * Validates user's password credentials against an internal user database. + * + */ +public interface UserValidator { + + /** + * Validates the given username/password combination. If no + * {@link UserValidationException} is thrown validation was successful. + * + * @param username The user to validate against the internal database. + * @param password The password of <code>username</code> + * @throws UserValidationException If the user could not be validated. Reasons + * as to why this exception may be thrown include: + * <ul> + * <li>The user does not exist in the database</li> + * <li>Invalid credentials were provided</li> + * </ul> + */ + void authenticate(String username, char[] password) throws UserValidationException; + + /** + * Get a read-only set of all users the system knows about. All members of + * the set are Strings. + * + * @return All known users. + * @throws IllegalStateException If no user database is available. + */ + Set<Object> getAllKnownUsers() throws IllegalStateException; +}
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java Tue May 14 18:09:15 2013 +0200 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java Fri May 03 19:14:14 2013 +0200 @@ -62,6 +62,8 @@ import java.util.HashMap; import java.util.Map; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; @@ -1019,6 +1021,34 @@ String[] insufficientRoles = new String[0]; doUnauthorizedTest("verify-token", failMsg, insufficientRoles, false); } + + @Test + public void initThrowsRuntimeExceptionIfThermostatHomeNotSet() { + // setup sets this, but we don't want to have it set for this test + System.clearProperty("THERMOSTAT_HOME"); + WebStorageEndPoint endpoint = new WebStorageEndPoint(); + ServletConfig config = mock(ServletConfig.class); + try { + endpoint.init(config); + fail("Thermostat home was not set in config, should not get here!"); + } catch (RuntimeException e) { + // pass + assertTrue(e.getMessage().contains("THERMOSTAT_HOME")); + } catch (ServletException e) { + fail(e.getMessage()); + } + // set config with non-existing dir + when(config.getInitParameter("THERMOSTAT_HOME")).thenReturn("not-existing"); + try { + endpoint.init(config); + fail("Thermostat home was set in config but file does not exist, should have died!"); + } catch (RuntimeException e) { + // pass + assertTrue(e.getMessage().contains("THERMOSTAT_HOME")); + } catch (ServletException e) { + fail(e.getMessage()); + } + } private byte[] verifyAuthorizedGenerateToken(String username, String password) throws IOException { String endpoint = getEndpoint();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/RolePrincipalTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,122 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import org.junit.Test; + +public class RolePrincipalTest { + + @Test + public void testEmptyRoles() { + RolePrincipal principal = new RolePrincipal("RolesContainer"); + assertEquals(false, principal.members().hasMoreElements()); + assertEquals(false, principal.isMember(new RolePrincipal("not-there"))); + assertEquals(false, principal.isMember(new UserPrincipal("user also not there"))); + assertEquals("RolesContainer", principal.getName()); + } + + @Test + public void canRemoveMembers() { + RolePrincipal principal = new RolePrincipal("Testing"); + assertFalse(principal.removeMember(new RolePrincipal("other"))); + assertFalse(principal.removeMember(new UserPrincipal("testuser"))); + RolePrincipal firstRoleMember = new RolePrincipal("first"); + RolePrincipal secondRoleMember = new RolePrincipal("second"); + RolePrincipal thirdRoleMember = new RolePrincipal("third"); + principal.addMember(firstRoleMember); + assertTrue("first member of Testing", principal.removeMember(firstRoleMember)); + principal.addMember(secondRoleMember); + principal.addMember(thirdRoleMember); + assertTrue("third member of Testing", principal.removeMember(thirdRoleMember)); + List<String> members = new ArrayList<>(); + @SuppressWarnings("unchecked") + Enumeration<Principal> roles = (Enumeration<Principal>) principal.members(); + while (roles.hasMoreElements()) { + members.add(roles.nextElement().getName()); + } + assertEquals(1, members.size()); + assertEquals("second", members.get(0)); + } + + @Test + public void rolePrincipalDoesNotAllowNonGroupsToBeAdded() { + RolePrincipal role = new RolePrincipal("test"); + try { + role.addMember(new UserPrincipal("something")); + fail("UserPrincipal not a Group, should not come here!"); + } catch (IllegalArgumentException e) { + // pass + } + } + + @Test( expected = NullPointerException.class ) + public void nullName() { + new RolePrincipal(null); + } + + @Test + public void isMemberWorksForNestedGroups() { + RolePrincipal principal = new RolePrincipal("Roles"); + RolePrincipal otherRole = new RolePrincipal("Nested 1"); + RolePrincipal thirdRole = new RolePrincipal("find-me"); + RolePrincipal notExistingRole = new RolePrincipal("notthere"); + otherRole.addMember(thirdRole); + principal.addMember(otherRole); + assertEquals("find-me member of Nested 1 which is member of Roles", true, principal.isMember(thirdRole)); + assertEquals(false, principal.isMember(notExistingRole)); + assertTrue(otherRole.removeMember(thirdRole)); + assertEquals("find-me no longer member of Nested 1 (which is still a member of Roles)", false, principal.isMember(thirdRole)); + } + + @Test + public void testEquals() { + UserPrincipal user = new UserPrincipal("test"); + RolePrincipal role = new RolePrincipal("test"); + assertFalse("Roles and users must not be equal", role.equals(user)); + assertTrue(role.equals(role)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/UserPrincipalTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,108 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import java.security.Principal; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; + +public class UserPrincipalTest { + + @Test(expected = NullPointerException.class) + public void testConstructor() { + new UserPrincipal(null); + } + + @Test + public void getName() { + UserPrincipal p = new UserPrincipal("testing"); + assertEquals("testing", p.getName()); + } + + @Test + public void canSetRoles() { + UserPrincipal p = new UserPrincipal("superuser"); + try { + p.setRoles(null); + fail("null roles not allowed"); + } catch (NullPointerException e) { + // pass + } + Set<BasicRole> roles = new HashSet<>(); + BasicRole role = mock(BasicRole.class); + roles.add(role); + p.setRoles(roles); + assertEquals(1, p.getRoles().size()); + } + + @Test + public void testEquals() { + UserPrincipal p = new UserPrincipal("testuser"); + assertTrue(p.equals(p)); + SimplePrincipal p2 = new SimplePrincipal("testuser"); + assertTrue(p2.equals(p)); + assertTrue(p.equals(p2)); + SimplePrincipal p3 = new SimplePrincipal("Tester"); + assertFalse(p2.equals(p3)); + assertFalse(p.equals(p3)); + Principal principal = new Principal() { + + @Override + public String getName() { + return "testuser"; + } + + }; + assertTrue(p.equals(principal)); + } + + @SuppressWarnings("serial") + private static class SimplePrincipal extends UserPrincipal { + + public SimplePrincipal(String name) { + super(name); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/WrappedRolePrincipalTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,85 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Enumeration; +import java.util.Vector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class WrappedRolePrincipalTest { + + private Group group; + @SuppressWarnings("rawtypes") + private Enumeration members = new Vector().elements(); + + @SuppressWarnings("unchecked") + @Before + public void setup() { + group = mock(Group.class); + when(group.addMember(any(Principal.class))).thenReturn(true); + when(group.isMember(any(Principal.class))).thenReturn(true); + when(group.getName()).thenReturn("group-under-the-hood"); + when(group.members()).thenReturn(members); + when(group.removeMember(any(Principal.class))).thenReturn(false); + } + + @After + public void tearDown() { + group = null; + } + + @Test + public void testAllDelegates() { + WrappedRolePrincipal p = new WrappedRolePrincipal(group); + Principal mock = mock(Principal.class); + assertEquals(true, p.addMember(mock)); + assertEquals(true, p.isMember(mock)); + assertEquals("group-under-the-hood", p.getName()); + assertEquals(members, p.members()); + assertEquals(false, p.removeMember(mock)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/AbstractLoginModuleTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,115 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; + +import org.junit.Test; + +public class AbstractLoginModuleTest { + + @Test + public void canGetUserNameFromCallBack() throws LoginException { + SimpleCallBackHandler handler = new SimpleCallBackHandler("testuser"); + LoginModuleImpl loginModule = new LoginModuleImpl(); + loginModule.initialize(new Subject(), handler, null, null); + assertEquals("testuser", loginModule.getUsernameFromCallBack()); + } + + @Test + public void canGetUserPasswordFromCallBack() throws LoginException { + SimpleCallBackHandler handler = new SimpleCallBackHandler("testuser", "testpassword".toCharArray()); + LoginModuleImpl loginModule = new LoginModuleImpl(); + loginModule.initialize(new Subject(), handler, null, null); + Object[] creds = loginModule.getUsernamePasswordFromCallBack(); + String user = (String)creds[0]; + char[] pwd = (char[])creds[1]; + assertEquals("testuser", user); + assertEquals("testpassword", new String(pwd)); + } + + private static class LoginModuleImpl extends AbstractLoginModule { + + @Override + public void initialize(Subject subject, + CallbackHandler callbackHandler, Map<String, ?> sharedState, + Map<String, ?> options) { + this.callBackHandler = callbackHandler; + } + + @Override + public boolean login() throws LoginException { + // don't care + return false; + } + + @Override + public boolean commit() throws LoginException { + // don't care + return false; + } + + @Override + public boolean abort() throws LoginException { + // don't care + return false; + } + + @Override + public boolean logout() throws LoginException { + // don't care + return false; + } + + @Override + public String getUsernameFromCallBack() throws LoginException { + return super.getUsernameFromCallBack(); + } + + @Override + public Object[] getUsernamePasswordFromCallBack() throws LoginException { + return super.getUsernamePasswordFromCallBack(); + } + + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/DelegateLoginModuleTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,177 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.net.URL; +import java.security.Principal; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.redhat.thermostat.web.server.auth.BasicRole; +import com.redhat.thermostat.web.server.auth.RolePrincipal; +import com.redhat.thermostat.web.server.auth.UserPrincipal; + +public class DelegateLoginModuleTest { + + private Subject subject; + private Map<String, Object> mockSharedState; + private Map<String, Object> mockOptions; + private CallbackHandler mockCallBack; + + @Before + public void setup() { + subject = new Subject(); + mockSharedState = new HashMap<>(); + mockOptions = new HashMap<>(); + // DelegateLoginModule uses the name callback + mockCallBack = new SimpleCallBackHandler("testUser", "doesn't matter".toCharArray()); + // sets jaas config so as to use StubDelegateLoginModule + URL testConfig = DelegateLoginModuleTest.class + .getResource("/delegate_login_module_test_jaas.conf"); + System.setProperty("java.security.auth.login.config", testConfig.getFile()); + } + + @After + public void teardown() { + subject = null; + mockSharedState = null; + mockOptions = null; + mockCallBack = null; + } + + @Test + public void testBasicsSuccess() throws Exception { + DelegateLoginModule delegateLogin = new DelegateLoginModule("Success"); + delegateLogin.initialize(subject, mockCallBack, mockSharedState, mockOptions); + assertTrue("Stub delegate in use is always successfull", delegateLogin.login()); + assertTrue("Stub delegate in use is always successfull", delegateLogin.commit()); + // SubSuccessDelegateLoginModule added one user principal + // 'testUser' and one role principal 'testRole'. + Subject subject = delegateLogin.getSubject(); + assertEquals(this.subject, subject); + Set<Principal> principals = subject.getPrincipals(); + assertEquals(2, principals.size()); + Iterator<Principal> it = principals.iterator(); + while (it.hasNext()) { + Principal p = it.next(); + if (p.getName().equals("testUser")) { + // user principal + assertTrue(p instanceof UserPrincipal); + UserPrincipal uPrincipal = (UserPrincipal)p; + assertNotNull("Should now be a thermostat UserPrincipal containing roles", uPrincipal.getRoles()); + assertEquals(1, uPrincipal.getRoles().size()); + Iterator<BasicRole> iter = uPrincipal.getRoles().iterator(); + while (iter.hasNext()) { + BasicRole role = iter.next(); + assertEquals("testRole", role.getName()); + } + } + if (p.getName().equals("testRole")) { + assertTrue("Should have been wrapped into a BasicRole", p instanceof BasicRole); + } + } + // Test the same another way for good measure. + assertTrue(principals.contains(new UserPrincipal("testUser"))); + assertTrue(principals.contains(new RolePrincipal("testRole"))); + + // now logout. we expect principals to be cleared + delegateLogin.logout(); + assertEquals(0, subject.getPrincipals().size()); + } + + @Test + public void testBasicsFailure() throws Exception { + DelegateLoginModule delegateLogin = new DelegateLoginModule("Failure"); + delegateLogin.initialize(subject, mockCallBack, mockSharedState, mockOptions); + // add some principal to the subject in order to make sure delegate + // clears principals on login failure. + Principal mock = new Principal() { + + @Override + public String getName() { + return "tester"; + } + + }; + subject.getPrincipals().add(mock); + assertEquals(1, subject.getPrincipals().size()); + try { + // this triggers abort() being called for the delegate. + delegateLogin.login(); + fail("StubFailureDelegateLoginModule should have thrown LE"); + } catch (LoginException e) { + // pass + } + assertEquals(0, subject.getPrincipals().size()); + } + + @Test + public void testAbort() throws LoginException { + // add some principal to the subject in order to make sure delegate + // clears principals on login failure. + Principal mock = new Principal() { + + @Override + public String getName() { + return "tester"; + } + + }; + subject.getPrincipals().add(mock); + assertEquals(1, subject.getPrincipals().size()); + DelegateLoginModule delegateLogin = new DelegateLoginModule("Failure"); + delegateLogin.initialize(subject, mockCallBack, mockSharedState, mockOptions); + // simulate abort which should clear any principals from the subject + delegateLogin.abort(); + assertEquals(0, subject.getPrincipals().size()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUserValidatorTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,148 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.redhat.thermostat.common.config.InvalidConfigurationException; + +public class PropertiesUserValidatorTest { + + private PropertiesUserValidator validator; + + @Before + public void setup() { + URL testFile = this.getClass().getResource("/test_users.properties"); + validator = new PropertiesUserValidator(testFile.getFile()); + } + + @After + public void tearDown() { + validator = null; + } + + + @Test + public void authenticationFailsForNonExistingUser() { + try { + validator.authenticate("does not exist", null); + fail("user does not exist in file"); + } catch (UserValidationException e) { + // pass + assertEquals("User 'does not exist' not found", e.getMessage()); + } + } + + @Test + public void passwordMatch() { + try { + validator.authenticate("user1", "passwd1".toCharArray()); + validator.authenticate("strange\nuser", "testMe".toCharArray()); + validator.authenticate("c102892{0}", "test".toCharArray()); + // extra spaces are remved from properties system + validator.authenticate("multiuser", "blah\ntest test".toCharArray()); + // '\0' in test_users.properties become '0' + validator.authenticate("strange0user", "test0Me".toCharArray()); + // pass + } catch (UserValidationException e) { + e.printStackTrace(); + fail("should have been able to authenticate user"); + } + } + + @Test + public void passwordMismatch() { + try { + validator.authenticate("user1", "passwD1".toCharArray()); + fail("password does not match!"); + } catch (UserValidationException e) { + assertEquals("Login failed!", e.getMessage()); + } + try { + validator.authenticate("c102892{0}", "passwd1".toCharArray()); + } catch (UserValidationException e) { + assertEquals("Login failed!", e.getMessage()); + } + } + + @Test + public void testInit() { + try { + new PropertiesUserValidator(); + fail("THERMOSTAT_HOME not set, should have failed!"); + } catch (InvalidConfigurationException e) { + // pass + assertTrue(e.getMessage().contains("THERMOSTAT_HOME")); + } + } + + @Test + public void testInitWithMissingFile() { + try { + new PropertiesUserValidator("file/which/does/not/exist"); + } catch (InvalidConfigurationException e) { + // pass + assertEquals("Unable to load user database", e.getMessage()); + } + } + + @Test + public void canGetUserSet() { + Set<String> expectedSet = new HashSet<>(10); + expectedSet.add("user1"); + expectedSet.add("testing"); + expectedSet.add("multiuser"); + expectedSet.add("c102892{0}"); + expectedSet.add("strange\nuser"); + expectedSet.add("strange0user"); + Set<Object> actual = validator.getAllKnownUsers(); + assertTrue(expectedSet.equals(actual)); + expectedSet.remove("user1"); + assertFalse(expectedSet.equals(actual)); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUsernameRolesLoginModuleTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,268 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.net.URL; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.redhat.thermostat.web.server.auth.RolePrincipal; +import com.redhat.thermostat.web.server.auth.UserPrincipal; + +public class PropertiesUsernameRolesLoginModuleTest { + + private Subject subject; + private Map<String, Object> mockSharedState; + private Map<String, Object> mockOptions; + private CallbackHandler mockCallBack; + + @Before + public void setup() { + subject = new Subject(); + mockSharedState = new HashMap<>(); + mockOptions = new HashMap<>(); + } + + @After + public void teardown() { + subject = null; + mockSharedState = null; + mockOptions = null; + mockCallBack = null; + } + + @Test + public void canInitialize() { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + mockCallBack = new SimpleCallBackHandler("testUser", "testpassword".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + try { + // this must not throw an exception + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + // pass + } catch (Exception e) { + fail("Did not expect any exception here!"); + } + } + + @Test + public void failsToLoginWithInvalidCredentials() { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + // testUser/testpassword not defined + mockCallBack = new SimpleCallBackHandler("testUser", "testpassword".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + try { + loginModule.login(); + fail("testUser not defined in properties_module_test_users.properties"); + } catch (LoginException e) { + // pass + assertEquals("User 'testUser' not found", e.getMessage()); + } + mockCallBack = new SimpleCallBackHandler("user1", "wrongpassword".toCharArray()); + loginModule = new PropertiesUsernameRolesLoginModule(); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + try { + loginModule.login(); + fail("user1 provided wrong password!"); + } catch (LoginException e) { + assertEquals("Login failed!", e.getMessage()); + } + } + + @Test + public void canLoginWithValidCredentials() { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + mockCallBack = new SimpleCallBackHandler("user1", "somepassword".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + try { + boolean retval = loginModule.login(); + // pass + assertTrue("Login should have returned true", retval); + } catch (LoginException e) { + fail("'user1' should be able to login"); + } + mockCallBack = new SimpleCallBackHandler("user2", "password".toCharArray()); + loginModule = new PropertiesUsernameRolesLoginModule(); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + try { + boolean retval = loginModule.login(); + // pass + assertTrue("Login should have returned true", retval); + } catch (LoginException e) { + fail("'user2' should be able to login"); + } + } + + @Test + public void canCommitOnSuccessfulLogin() throws LoginException { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + mockCallBack = new SimpleCallBackHandler("user1", "somepassword".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + assertTrue(loginModule.login()); + try { + boolean retval = loginModule.commit(); + // pass + assertTrue("Commit should have returned true", retval); + } catch (LoginException e) { + fail("'user1' should have been able to commit"); + } + // assert principals are added to subject + Set<Principal> principals = subject.getPrincipals(); + // my-role, my-role2, Roles, user1 + assertEquals(4, principals.size()); + assertTrue(principals.contains(new RolePrincipal("my-role"))); + assertTrue(principals.contains(new RolePrincipal("my-role2"))); + assertTrue(principals.contains(new RolePrincipal("Roles"))); + assertTrue(principals.contains(new UserPrincipal("user1"))); + } + + @Test + public void testRecursiveRoles() throws LoginException { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + mockCallBack = new SimpleCallBackHandler("user2", "password".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + assertTrue(loginModule.login()); + try { + boolean retval = loginModule.commit(); + // pass + assertTrue("Commit should have returned true", retval); + } catch (LoginException e) { + fail("'user2' should have been able to commit"); + } + // assert principals are added to subject + Set<Principal> principals = subject.getPrincipals(); + // new-role, role1, other-role, Roles, user2 + assertEquals(5, principals.size()); + assertTrue(principals.contains(new RolePrincipal("new-role"))); + assertTrue(principals.contains(new RolePrincipal("role1"))); + assertTrue(principals.contains(new RolePrincipal("Roles"))); + // via recursive role 'role1' + assertTrue(principals.contains(new RolePrincipal("other-role"))); + assertTrue(principals.contains(new UserPrincipal("user2"))); + } + + @Test + public void cannotCommitWithoutLoggingIn() throws LoginException { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + assertFalse(loginModule.commit()); + + loginModule = new PropertiesUsernameRolesLoginModule(); + mockCallBack = new SimpleCallBackHandler("user1", "somepassword".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + assertFalse(loginModule.commit()); + assertEquals(0, subject.getPrincipals().size()); + } + + @Test + public void testLoginCommitAbort() throws LoginException { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + mockCallBack = new SimpleCallBackHandler("user1", "somepassword".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + assertTrue(loginModule.login()); + assertTrue(loginModule.commit()); + // assert principals are added to subject after login + Set<Principal> principals = subject.getPrincipals(); + // my-role, my-role2, Roles, user1 + assertEquals(4, principals.size()); + assertTrue(loginModule.abort()); + // make sure principals are cleared + assertEquals(0, principals.size()); + } + + @Test + public void testLoginCommitLogout() throws LoginException { + LoginModule loginModule = new PropertiesUsernameRolesLoginModule(); + mockCallBack = new SimpleCallBackHandler("user1", "somepassword".toCharArray()); + URL userFile = this.getClass().getResource("/properties_module_test_users.properties"); + URL rolesFile = this.getClass().getResource("/properties_module_test_roles.properties"); + mockOptions.put("users.properties", userFile.getFile()); + mockOptions.put("roles.properties", rolesFile.getFile()); + loginModule.initialize(subject, mockCallBack, mockSharedState, mockOptions); + assertTrue(loginModule.login()); + assertTrue(loginModule.commit()); + // assert principals are added to subject after login + Set<Principal> principals = subject.getPrincipals(); + // my-role, my-role2, Roles, user1 + assertEquals(4, principals.size()); + assertTrue(loginModule.logout()); + // make sure principals are cleared + assertEquals(0, principals.size()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/RolesAmenderTest.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,210 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.net.URL; +import java.security.Principal; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.redhat.thermostat.common.config.InvalidConfigurationException; +import com.redhat.thermostat.web.server.auth.BasicRole; +import com.redhat.thermostat.web.server.auth.RolePrincipal; +import com.redhat.thermostat.web.server.auth.spi.RolesAmender.RolesInfo; + +public class RolesAmenderTest { + + private RolesAmender rolesAmender; + private String validFile; + private Set<Object> validUsers; + + @Before + public void setup() { + URL testFile = this.getClass().getResource("/test_roles.properties"); + validFile = testFile.getFile(); + Set<Object> users = new HashSet<>(); + users.add("user1"); + users.add("user2"); + validUsers = Collections.unmodifiableSet(users); + } + + @After + public void tearDown() { + rolesAmender = null; + validFile = null; + } + + @Test + public void canParseRoles() { + rolesAmender = new RolesAmender(validFile, validUsers); + Set<BasicRole> roles = rolesAmender.getRoles("user1"); + assertEquals(2, roles.size()); + Map<String, Boolean> userRolesMap = new HashMap<>(); + Iterator<BasicRole> it = roles.iterator(); + while (it.hasNext()) { + BasicRole role = it.next(); + // don't expect role nesting + assertTrue(role instanceof RolePrincipal); + assertFalse(role.members().hasMoreElements()); + userRolesMap.put(role.getName(), true); + } + assertTrue(userRolesMap.containsKey("my-role")); + assertTrue(userRolesMap.containsKey("my-role2")); + roles = rolesAmender.getRoles("user2"); + // new-role, role1, other-role + assertEquals(3, roles.size()); + it = roles.iterator(); + while (it.hasNext()) { + BasicRole role = it.next(); + assertTrue(role instanceof RolePrincipal); + if (role.getName().equals("role1")) { + // nested role + int count = 0; + @SuppressWarnings("rawtypes") + Enumeration members = role.members(); + while (members.hasMoreElements()) { + count++; + assertEquals("other-role", ((Principal)members.nextElement()).getName()); + } + assertEquals(1, count); + } + if (role.getName().equals("new-role")) { + // not nested + assertFalse(role.members().hasMoreElements()); + } + } + assertTrue(roles.contains(new RolePrincipal("other-role"))); + // role1 is not a user + roles = rolesAmender.getRoles("role1"); + // expect empty + assertEquals(0, roles.size()); + } + + @Test + public void canParseRolesWithMoreUsersThanUsedInRoles() { + Set<Object> users = new HashSet<>(); + users.add("user1"); + users.add("user2"); + users.add("user-with-no-roles"); + validUsers = Collections.unmodifiableSet(users); + try { + rolesAmender = new RolesAmender(validFile, validUsers); + } catch (Exception e) { + fail("should be able to parse roles"); + } + Set<BasicRole> roles = rolesAmender.getRoles("user-with-no-roles"); + assertEquals(0, roles.size()); + } + + @Test + public void parseFailsIfUserInRecursiveRole() { + String brokenFile = this.getClass().getResource("/broken_test_roles.properties").getFile(); + try { + rolesAmender = new RolesAmender(brokenFile, validUsers); + fail("Should not parse"); + } catch (Exception e) { + assertTrue(e instanceof IllegalStateException); + assertEquals("Failed to parse roles", e.getMessage()); + } + } + + @Test + public void parseFailsIfNotAnyUserMemberOfRecursiveRole() { + String brokenFile = this.getClass().getResource("/broken_test_roles2.properties").getFile(); + try { + rolesAmender = new RolesAmender(brokenFile, validUsers); + fail("Should not parse"); + } catch (Exception e) { + assertTrue(e instanceof IllegalStateException); + assertEquals("Failed to parse roles", e.getMessage()); + } + } + + @Test + public void testInit() { + try { + new RolesAmender(null); + fail("THERMOSTAT_HOME not set, should have failed!"); + } catch (InvalidConfigurationException e) { + // pass + assertTrue(e.getMessage().contains("THERMOSTAT_HOME")); + } + } + + @Test + public void testInitWithMissingFile() { + try { + new RolesAmender("file/which/does/not/exist", validUsers); + } catch (InvalidConfigurationException e) { + // pass + assertEquals("Failed to load roles from properties", e.getMessage()); + } + } + + @Test(expected = NullPointerException.class) + public void testInitNull() { + new RolesAmender("not-important", null); + } + + @Test + public void rolesInfoTest() { + RolePrincipal p = new RolePrincipal("test-role"); + RolesInfo info = new RolesInfo(p); + assertEquals("test-role", info.getRole().getName()); + assertEquals(0, info.getMemberUsers().size()); + Set<String> roleMembers = info.getMemberUsers(); + roleMembers.add("testuser"); + assertEquals(1, info.getMemberUsers().size()); + assertEquals(true, info.getMemberUsers().contains("testuser")); + roleMembers.add("testuser"); + assertEquals(1, info.getMemberUsers().size()); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/SimpleCallBackHandler.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,77 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.io.IOException; + +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; + +class SimpleCallBackHandler implements CallbackHandler { + + private final String username; + private final char[] password; + + SimpleCallBackHandler(String username) { + this.username = username; + this.password = null; + } + + SimpleCallBackHandler(String username, char[] password) { + this.username = username; + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, + UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + Callback cb = callbacks[i]; + if (cb instanceof NameCallback) { + ((NameCallback) cb).setName(username); + } + if (cb instanceof PasswordCallback) { + ((PasswordCallback) cb).setPassword(password); + } + } + + } + +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/StubFailureDelegateLoginModule.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,88 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +public class StubFailureDelegateLoginModule implements LoginModule { + + private boolean initialized = false; + private boolean loginCalled = false; + private Subject subject; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map<String, ?> sharedState, Map<String, ?> options) { + this.subject = subject; + this.initialized = true; + } + + @Override + public boolean login() throws LoginException { + if (!initialized) { + throw new AssertionError("logging in when not initialized"); + } + loginCalled = true; + throw new LoginException("Failed to login"); + } + + @Override + public boolean commit() throws LoginException { + throw new AssertionError("should never be called"); + } + + @Override + public boolean abort() throws LoginException { + if (!loginCalled) { + throw new AssertionError("should have called login first"); + } + subject.getPrincipals().clear(); + return true; + } + + @Override + public boolean logout() throws LoginException { + // don't care + return true; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/StubSuccessDelegateLoginModule.java Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,134 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * <http://www.gnu.org/licenses/>. + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.web.server.auth.spi; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Enumeration; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +public class StubSuccessDelegateLoginModule implements LoginModule { + + private boolean initialized = false; + private boolean loggedIn = false; + private Subject subject; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map<String, ?> sharedState, Map<String, ?> options) { + this.subject = subject; + this.initialized = true; + } + + @Override + public boolean login() throws LoginException { + if (!initialized) { + throw new AssertionError("logging in when not initialized"); + } + this.loggedIn = true; + return true; + } + + @Override + public boolean commit() throws LoginException { + if (!loggedIn) { + throw new AssertionError("committing when not logged in"); + } + // fake adding two principals. One user and one role + // principal. + Set<Principal> principals = this.subject.getPrincipals(); + principals.add(new Principal() { + + @Override + public String getName() { + return "testUser"; + } + + }); + principals.add(new Group() { + + @Override + public String getName() { + return "testRole"; + } + + @Override + public boolean addMember(Principal user) { + // don't care + return false; + } + + @Override + public boolean removeMember(Principal user) { + // don't care + return false; + } + + @Override + public boolean isMember(Principal member) { + // don't care + return false; + } + + @Override + public Enumeration<? extends Principal> members() { + Vector<Principal> stub = new Vector<>(); + return stub.elements(); + } + + }); + return true; + } + + @Override + public boolean abort() throws LoginException { + throw new AssertionError("Should never be called"); + } + + @Override + public boolean logout() throws LoginException { + return true; + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/resources/broken_test_roles.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,3 @@ +user1 = my-role, my-role2 +user2 = new-role, role1 +role1 = other-role, user1 \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/resources/broken_test_roles2.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,5 @@ +# Defines recursive role, but no user is a member of it. +# Therefore, should fail to parse +user1 = my-role, my-role2 +user2 = new-role +role1 = other-role, user1 \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/resources/delegate_login_module_test_jaas.conf Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,7 @@ +Success { + com.redhat.thermostat.web.server.auth.spi.StubSuccessDelegateLoginModule required debug=true; +}; + +Failure { + com.redhat.thermostat.web.server.auth.spi.StubFailureDelegateLoginModule required debug=true; +}; \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/resources/properties_module_test_roles.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,3 @@ +user1 = my-role, my-role2 +user2 = new-role, role1 +role1 = other-role \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/resources/properties_module_test_users.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,2 @@ +user1 = somepassword +user2 = password \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/resources/test_roles.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,4 @@ +user1 = my-role, my-role2 +# empty should get skipped (', ,') +user2 = new-role, role1, , +role1 = other-role \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/server/src/test/resources/test_users.properties Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,9 @@ +user1=passwd1 +# comment +testing = password +multiuser = blah\ntest \ + test +c102892{0} = test +strange\nuser = testMe + +strange\0user = test\0Me
--- a/web/war/pom.xml Tue May 14 18:09:15 2013 +0200 +++ b/web/war/pom.xml Fri May 03 19:14:14 2013 +0200 @@ -51,22 +51,51 @@ <name>Thermostat Web WAR package</name> <dependencies> - - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> <dependency> <groupId>com.redhat.thermostat</groupId> <artifactId>thermostat-distribution</artifactId> <version>${project.version}</version> - </dependency> - <dependency> - <groupId>javax.servlet</groupId> - <artifactId>servlet-api</artifactId> - <version>${javax.servlet.version}</version> - <scope>provided</scope> + <!-- exclude conflicting jars. Those cause problems for servlet + containers and hence prevent the web archive to deploy + properly --> + <exclusions> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-webapp</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-util</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-continuation</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-http</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-io</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-security</artifactId> + </exclusion> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-xml</artifactId> + </exclusion> + </exclusions> </dependency> </dependencies>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/war/src/main/webapp/META-INF/context.xml Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- allow symlinks to /usr/share/java deps in WEB-INF/lib --> +<Context path="/thermostat" allowLinking="true"> + <!-- Configures JAAS for Tomcat --> + <Realm className="org.apache.catalina.realm.JAASRealm" + appName="ThermostatJAASLogin" + userClassNames="com.redhat.thermostat.web.server.auth.UserPrincipal" + roleClassNames="com.redhat.thermostat.web.server.auth.WrappedRolePrincipal, + com.redhat.thermostat.web.server.auth.RolePrincipal"/> +</Context>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/war/src/main/webapp/WEB-INF/jetty-web.xml Fri May 03 19:14:14 2013 +0200 @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"> + +<!-- ================================================================== +Thermostat JAAS configuration for jetty. This tells jetty that +it should use JAAS for the "Thermostat Realm". It is imperative that +the realm name matches. It also tells jetty which classes map to +role principals and set the name of the JAAS config entry to use. + +This should work with Jetty 8. + +===================================================================== --> + +<Configure class="org.eclipse.jetty.webapp.WebAppContext"> + <Set name="contextPath">/thermostat</Set> + <Get name="securityHandler"> + <Set name="loginService"> + <New class="org.eclipse.jetty.plus.jaas.JAASLoginService"> + <Set name="name">Thermostat Realm</Set> + <Set name="loginModuleName">ThermostatJAASLogin</Set> + <Set name="roleClassNames"> + <Array type="java.lang.String"> + <Item>com.redhat.thermostat.web.server.auth.RolePrincipal</Item> + <Item>com.redhat.thermostat.web.server.auth.WrappedRolePrincipal</Item> + </Array> + </Set> + </New> + </Set> + </Get> +</Configure>