You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shiro.apache.org by lh...@apache.org on 2011/07/22 22:34:29 UTC
svn commit: r1149728 - in /shiro/trunk/core: pom.xml
src/main/java/org/apache/shiro/realm/jdbc/JdbcRealm.java
src/test/java/org/apache/shiro/realm/jdbc/
src/test/java/org/apache/shiro/realm/jdbc/JDBCRealmTest.java
Author: lhazlewood
Date: Fri Jul 22 20:34:28 2011
New Revision: 1149728
URL: http://svn.apache.org/viewvc?rev=1149728&view=rev
Log:
SHIRO-277: applied patch
Added:
shiro/trunk/core/src/test/java/org/apache/shiro/realm/jdbc/
shiro/trunk/core/src/test/java/org/apache/shiro/realm/jdbc/JDBCRealmTest.java
Modified:
shiro/trunk/core/pom.xml
shiro/trunk/core/src/main/java/org/apache/shiro/realm/jdbc/JdbcRealm.java
Modified: shiro/trunk/core/pom.xml
URL: http://svn.apache.org/viewvc/shiro/trunk/core/pom.xml?rev=1149728&r1=1149727&r2=1149728&view=diff
==============================================================================
--- shiro/trunk/core/pom.xml (original)
+++ shiro/trunk/core/pom.xml Fri Jul 22 20:34:28 2011
@@ -100,6 +100,12 @@
</exclusion>
</exclusions>
</dependency>
+ <!-- JDBC Realm tests: -->
+ <dependency>
+ <groupId>hsqldb</groupId>
+ <artifactId>hsqldb</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
Modified: shiro/trunk/core/src/main/java/org/apache/shiro/realm/jdbc/JdbcRealm.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/main/java/org/apache/shiro/realm/jdbc/JdbcRealm.java?rev=1149728&r1=1149727&r2=1149728&view=diff
==============================================================================
--- shiro/trunk/core/src/main/java/org/apache/shiro/realm/jdbc/JdbcRealm.java (original)
+++ shiro/trunk/core/src/main/java/org/apache/shiro/realm/jdbc/JdbcRealm.java Fri Jul 22 20:34:28 2011
@@ -25,6 +25,7 @@ import org.apache.shiro.authz.SimpleAuth
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.JdbcUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,7 +36,6 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
-import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -65,6 +65,11 @@ public class JdbcRealm extends Authorizi
* The default query used to retrieve account data for the user.
*/
protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
+
+ /**
+ * The default query used to retrieve account data for the user when {@link #saltStyle} is COLUMN.
+ */
+ protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
/**
* The default query used to retrieve the roles that apply to a user.
@@ -77,6 +82,16 @@ public class JdbcRealm extends Authorizi
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
private static final Logger log = LoggerFactory.getLogger(JdbcRealm.class);
+
+ /**
+ * Password hash salt configuration. <ul>
+ * <li>NO_SALT - password hashes are not salted.</li>
+ * <li>CRYTP - password hashes are stored in in the database unix crypt format.</li>
+ * <li>COLUMN - salt is in a separate column in the database.</li>
+ * <li>EXTERNAL - salt is not stored in the database. {@link #getSaltForUser(String)} will be called
+ * to get the salt</li></ul>
+ */
+ public enum SaltStyle {NO_SALT, CRYPT, COLUMN, EXTERNAL};
/*--------------------------------------------
| I N S T A N C E V A R I A B L E S |
@@ -90,6 +105,8 @@ public class JdbcRealm extends Authorizi
protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY;
protected boolean permissionsLookupEnabled = false;
+
+ protected SaltStyle saltStyle = SaltStyle.NO_SALT;
/*--------------------------------------------
| C O N S T R U C T O R S |
@@ -98,7 +115,7 @@ public class JdbcRealm extends Authorizi
/*--------------------------------------------
| A C C E S S O R S / M O D I F I E R S |
============================================*/
-
+
/**
* Sets the datasource that should be used to retrieve connections used by this realm.
*
@@ -165,6 +182,18 @@ public class JdbcRealm extends Authorizi
public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) {
this.permissionsLookupEnabled = permissionsLookupEnabled;
}
+
+ /**
+ * Sets the salt style. See {@link #saltStyle}.
+ *
+ * @param saltStyle new SaltStyle to set.
+ */
+ public void setSaltStyle(SaltStyle saltStyle) {
+ this.saltStyle = saltStyle;
+ if (saltStyle == SaltStyle.COLUMN && authenticationQuery.equals(DEFAULT_AUTHENTICATION_QUERY)) {
+ authenticationQuery = DEFAULT_SALTED_AUTHENTICATION_QUERY;
+ }
+ }
/*--------------------------------------------
| M E T H O D S |
@@ -181,17 +210,39 @@ public class JdbcRealm extends Authorizi
}
Connection conn = null;
- AuthenticationInfo info = null;
+ SimpleAuthenticationInfo info = null;
try {
conn = dataSource.getConnection();
- String password = getPasswordForUser(conn, username);
+ String password = null;
+ String salt = null;
+ switch (saltStyle) {
+ case NO_SALT:
+ password = getPasswordForUser(conn, username)[0];
+ break;
+ case CRYPT:
+ // TODO: separate password and hash from getPasswordForUser[0]
+ throw new ConfigurationException("Not implemented yet");
+ //break;
+ case COLUMN:
+ String[] queryResults = getPasswordForUser(conn, username);
+ password = queryResults[0];
+ salt = queryResults[1];
+ break;
+ case EXTERNAL:
+ password = getPasswordForUser(conn, username)[0];
+ salt = getSaltForUser(username);
+ }
if (password == null) {
throw new UnknownAccountException("No account found for user [" + username + "]");
}
- info = buildAuthenticationInfo(username, password.toCharArray());
+ info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
+
+ if (salt != null) {
+ info.setCredentialsSalt(ByteSource.Util.bytes(salt));
+ }
} catch (SQLException e) {
final String message = "There was a SQL error while authenticating user [" + username + "]";
@@ -208,15 +259,23 @@ public class JdbcRealm extends Authorizi
return info;
}
- protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
- return new SimpleAuthenticationInfo(username, password, getName());
- }
-
- private String getPasswordForUser(Connection conn, String username) throws SQLException {
+ private String[] getPasswordForUser(Connection conn, String username) throws SQLException {
+ String[] result;
+ boolean returningSeparatedSalt = false;
+ switch (saltStyle) {
+ case NO_SALT:
+ case CRYPT:
+ case EXTERNAL:
+ result = new String[1];
+ break;
+ default:
+ result = new String[2];
+ returningSeparatedSalt = true;
+ }
+
PreparedStatement ps = null;
ResultSet rs = null;
- String password = null;
try {
ps = conn.prepareStatement(authenticationQuery);
ps.setString(1, username);
@@ -233,7 +292,10 @@ public class JdbcRealm extends Authorizi
throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique.");
}
- password = rs.getString(1);
+ result[0] = rs.getString(1);
+ if (returningSeparatedSalt) {
+ result[1] = rs.getString(2);
+ }
foundResult = true;
}
@@ -242,7 +304,7 @@ public class JdbcRealm extends Authorizi
JdbcUtils.closeStatement(ps);
}
- return password;
+ return result;
}
/**
@@ -357,5 +419,9 @@ public class JdbcRealm extends Authorizi
return permissions;
}
+
+ protected String getSaltForUser(String username) {
+ return username;
+ }
}
Added: shiro/trunk/core/src/test/java/org/apache/shiro/realm/jdbc/JDBCRealmTest.java
URL: http://svn.apache.org/viewvc/shiro/trunk/core/src/test/java/org/apache/shiro/realm/jdbc/JDBCRealmTest.java?rev=1149728&view=auto
==============================================================================
--- shiro/trunk/core/src/test/java/org/apache/shiro/realm/jdbc/JDBCRealmTest.java (added)
+++ shiro/trunk/core/src/test/java/org/apache/shiro/realm/jdbc/JDBCRealmTest.java Fri Jul 22 20:34:28 2011
@@ -0,0 +1,373 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.shiro.realm.jdbc;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.IncorrectCredentialsException;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.config.IniSecurityManagerFactory;
+import org.apache.shiro.crypto.hash.Sha256Hash;
+import org.apache.shiro.mgt.DefaultSecurityManager;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.JdbcUtils;
+import org.apache.shiro.util.ThreadContext;
+import org.hsqldb.jdbc.jdbcDataSource;
+import org.junit.*;
+import org.junit.rules.TestName;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+
+
+/**
+ * Test case for JDBCRealm.
+ */
+public class JDBCRealmTest {
+
+ protected DefaultSecurityManager securityManager = null;
+ protected AuthorizingRealm realm;
+ protected final String username = "testUser";
+ protected final String plainTextPassword = "testPassword";
+ protected final String salt = username; //Default impl of getSaltForUser returns username
+ protected final String testRole = "testRole";
+ protected final String testPermissionString = "testDomain:testTarget:testAction";
+
+ // Maps keyed on test method name so setup/teardown can manage per test resources
+ protected HashMap<String, JdbcRealm> realmMap = new HashMap<String, JdbcRealm>();
+ protected HashMap<String, DataSource> dsMap = new HashMap<String, DataSource>();
+
+ @Rule
+ public TestName name = new TestName();
+
+ @Before
+ public void setup() {
+ ThreadContext.remove();
+ Ini config = new Ini();
+ config.setSectionProperty("main", "myRealm", "org.apache.shiro.realm.jdbc.JdbcRealm");
+ config.setSectionProperty("main", "myRealmCredentialsMatcher", "org.apache.shiro.authc.credential.Sha256CredentialsMatcher");
+ config.setSectionProperty("main", "myRealm.credentialsMatcher", "$myRealmCredentialsMatcher");
+ config.setSectionProperty("main", "securityManager.sessionManager.sessionValidationSchedulerEnabled", "false");
+
+ IniSecurityManagerFactory factory = new IniSecurityManagerFactory(config);
+ securityManager = (DefaultSecurityManager) factory.createInstance();
+ SecurityUtils.setSecurityManager(securityManager);
+
+ // Create a database and realm for the test
+ createRealm(name.getMethodName());
+ }
+
+ @After
+ public void tearDown() {
+ final String testName = name.getMethodName();
+ shutDown(testName);
+ SecurityUtils.setSecurityManager(null);
+ securityManager.destroy();
+ ThreadContext.remove();
+ }
+
+ @Test
+ public void testUnSaltedSuccess() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, false);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.NO_SALT);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, plainTextPassword);
+ currentUser.login(token);
+ currentUser.logout();
+ }
+
+ @Test
+ public void testUnSaltedWrongPassword() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, false);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.NO_SALT);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, "passwrd");
+ try {
+ currentUser.login(token);
+ } catch (IncorrectCredentialsException ex) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testUnSaltedMultipleRows() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, false);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.NO_SALT);
+ Connection conn = dsMap.get(testMethodName).getConnection();
+ Statement sql = conn.createStatement();
+ sql.executeUpdate("insert into users values ('" + username + "', 'dupe')");
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, "passwrd");
+ try {
+ currentUser.login(token);
+ } catch (AuthenticationException ex) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testSaltColumnSuccess() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createSaltColumnSchema(testMethodName);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.COLUMN);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, plainTextPassword);
+ currentUser.login(token);
+ currentUser.logout();
+ }
+
+ @Test
+ public void testSaltColumnWrongPassword() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createSaltColumnSchema(testMethodName);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.COLUMN);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, "passwrd");
+ try {
+ currentUser.login(token);
+ } catch (IncorrectCredentialsException ex) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testExternalSuccess() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, true);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.EXTERNAL);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, plainTextPassword);
+ currentUser.login(token);
+ currentUser.logout();
+ }
+
+ @Test
+ public void testExternalWrongPassword() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, true);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.EXTERNAL);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, "passwrd");
+ try {
+ currentUser.login(token);
+ } catch (IncorrectCredentialsException ex) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testRolePresent() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, false);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.NO_SALT);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, plainTextPassword);
+ currentUser.login(token);
+ Assert.assertTrue(currentUser.hasRole(testRole));
+ }
+
+ @Test
+ public void testRoleNotPresent() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, false);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.NO_SALT);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, plainTextPassword);
+ currentUser.login(token);
+ Assert.assertFalse(currentUser.hasRole("Game Overall Director"));
+ }
+
+ @Test
+ public void testPermissionPresent() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, false);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.NO_SALT);
+ realm.setPermissionsLookupEnabled(true);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, plainTextPassword);
+ currentUser.login(token);
+ Assert.assertTrue(currentUser.isPermitted(testPermissionString));
+ }
+
+ @Test
+ public void testPermissionNotPresent() throws Exception {
+ String testMethodName = name.getMethodName();
+ JdbcRealm realm = realmMap.get(testMethodName);
+ createDefaultSchema(testMethodName, false);
+ realm.setSaltStyle(JdbcRealm.SaltStyle.NO_SALT);
+ realm.setPermissionsLookupEnabled(true);
+
+ Subject.Builder builder = new Subject.Builder(securityManager);
+ Subject currentUser = builder.buildSubject();
+ UsernamePasswordToken token = new UsernamePasswordToken(username, plainTextPassword);
+ currentUser.login(token);
+ Assert.assertFalse(currentUser.isPermitted("testDomain:testTarget:specialAction"));
+ }
+
+ /**
+ * Creates a realm for a test method and puts it in the realMap.
+ */
+ protected void createRealm(String testMethodName) {
+ JdbcRealm realm = (JdbcRealm) securityManager.getRealms().iterator().next();
+ realmMap.put(testMethodName, realm);
+ }
+
+ /**
+ * Shuts down the database and removes the realm from the realm map.
+ */
+ protected void shutDown(String testName) {
+ Connection conn = null;
+ Statement sql = null;
+ DataSource ds = dsMap.get(testName);
+ try {
+ Connection c = ds.getConnection();
+ Statement s = c.createStatement();
+ s.executeUpdate("SHUTDOWN");
+ } catch (SQLException ex) {
+ // ignore
+ } finally {
+ JdbcUtils.closeStatement(sql);
+ JdbcUtils.closeConnection(conn);
+ dsMap.remove(testName);
+ realmMap.remove(testName);
+ }
+ }
+
+ /**
+ * Creates a test database with the default (no separate salt column) schema, salting with
+ * username if salted is true. Sets the DataSource of the realm associated with the test
+ * to a DataSource connected to the database. (To prevent concurrency problems when tests
+ * are executed in multithreaded mode, each test method gets its own database.)
+ */
+ protected void createDefaultSchema(String testName, boolean salted) {
+ jdbcDataSource ds = new jdbcDataSource();
+ ds.setDatabase("jdbc:hsqldb:mem:" + name);
+ ds.setUser("SA");
+ ds.setPassword("");
+ Connection conn = null;
+ Statement sql = null;
+ try {
+ conn = ds.getConnection();
+ sql = conn.createStatement();
+ sql.executeUpdate("create table users (username varchar(20), password varchar(20))");
+ Sha256Hash sha256Hash = salted ? new Sha256Hash(plainTextPassword, salt) :
+ new Sha256Hash(plainTextPassword);
+ String password = sha256Hash.toHex();
+ sql.executeUpdate("insert into users values ('" + username + "', '" + password + "')");
+ } catch (SQLException ex) {
+ Assert.fail("Exception creating test database");
+ } finally {
+ JdbcUtils.closeStatement(sql);
+ JdbcUtils.closeConnection(conn);
+ }
+ createRolesAndPermissions(ds);
+ realmMap.get(testName).setDataSource(ds);
+ dsMap.put(testName, ds);
+ }
+
+ /**
+ * Creates a test database with a separate salt column in the users table. Sets the
+ * DataSource of the realm associated with the test to a DataSource connected to the database.
+ */
+ protected void createSaltColumnSchema(String testName) {
+ jdbcDataSource ds = new jdbcDataSource();
+ ds.setDatabase("jdbc:hsqldb:mem:" + name);
+ ds.setUser("SA");
+ ds.setPassword("");
+ Connection conn = null;
+ Statement sql = null;
+ try {
+ conn = ds.getConnection();
+ sql = conn.createStatement();
+ sql.executeUpdate(
+ "create table users (username varchar(20), password varchar(20), password_salt varchar(20))");
+ Sha256Hash sha256Hash = new Sha256Hash(plainTextPassword, salt);
+ String password = sha256Hash.toHex();
+ sql.executeUpdate("insert into users values ('" + username + "', '" + password + "', '" + salt + "')");
+ } catch (SQLException ex) {
+ Assert.fail("Exception creating test database");
+ } finally {
+ JdbcUtils.closeStatement(sql);
+ JdbcUtils.closeConnection(conn);
+ }
+ createRolesAndPermissions(ds);
+ realmMap.get(testName).setDataSource(ds);
+ dsMap.put(testName, ds);
+ }
+
+ /**
+ * Creates and adds test data to user_role and roles_permissions tables.
+ */
+ protected void createRolesAndPermissions(DataSource ds) {
+ Connection conn = null;;
+ Statement sql = null;
+ try {
+ conn = ds.getConnection();
+ sql = conn.createStatement();
+ sql.executeUpdate("create table user_roles (username varchar(20), role_name varchar(20))");
+ sql.executeUpdate("insert into user_roles values ('" + username + "', '" + testRole + "')");
+ sql.executeUpdate("create table roles_permissions (role_name varchar(20), permission varchar(40))");
+ sql.executeUpdate(
+ "insert into roles_permissions values ('" + testRole + "', '" + testPermissionString + "')");
+ } catch (SQLException ex) {
+ Assert.fail("Exception adding test role and permission");
+ } finally {
+ JdbcUtils.closeStatement(sql);
+ JdbcUtils.closeConnection(conn);
+ }
+ }
+}