changeset 1087:105617e21576

Implement JAAS based authentication. Reviewed-by: ebaron, neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-May/006508.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Fri, 03 May 2013 19:14:14 +0200
parents 75a475d10f7e
children 3a0db0e80562
files distribution/config/thermostat-roles.properties distribution/config/thermostat-users.properties distribution/config/thermostat_jaas.conf distribution/pom.xml integration-tests/pom.xml integration-tests/src/test/java/com/redhat/thermostat/itest/IntegrationTest.java integration-tests/src/test/java/com/redhat/thermostat/itest/WebAppTest.java web/client/src/main/java/com/redhat/thermostat/web/client/internal/WebStorage.java web/server/pom.xml web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/BasicRole.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/RolePrincipal.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/UserPrincipal.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/WrappedRolePrincipal.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/AbstractLoginModule.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/DelegateLoginModule.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUserValidator.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUsernameRolesLoginModule.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/RolesAmender.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/UserValidationException.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/spi/UserValidator.java web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/RolePrincipalTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/UserPrincipalTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/WrappedRolePrincipalTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/AbstractLoginModuleTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/DelegateLoginModuleTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUserValidatorTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/PropertiesUsernameRolesLoginModuleTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/RolesAmenderTest.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/SimpleCallBackHandler.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/StubFailureDelegateLoginModule.java web/server/src/test/java/com/redhat/thermostat/web/server/auth/spi/StubSuccessDelegateLoginModule.java web/server/src/test/resources/broken_test_roles.properties web/server/src/test/resources/broken_test_roles2.properties web/server/src/test/resources/delegate_login_module_test_jaas.conf web/server/src/test/resources/properties_module_test_roles.properties web/server/src/test/resources/properties_module_test_users.properties web/server/src/test/resources/test_roles.properties web/server/src/test/resources/test_users.properties web/war/pom.xml web/war/src/main/webapp/META-INF/context.xml web/war/src/main/webapp/WEB-INF/jetty-web.xml
diffstat 43 files changed, 3271 insertions(+), 49 deletions(-) [+]
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>