/* | |
* 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.jsecurity.realm.jdbc; | |
import org.apache.commons.logging.Log; | |
import org.apache.commons.logging.LogFactory; | |
import org.jsecurity.authc.*; | |
import org.jsecurity.authz.AuthorizationException; | |
import org.jsecurity.authz.AuthorizationInfo; | |
import org.jsecurity.authz.SimpleAuthorizationInfo; | |
import org.jsecurity.realm.AuthorizingRealm; | |
import org.jsecurity.subject.PrincipalCollection; | |
import org.jsecurity.util.JdbcUtils; | |
import javax.sql.DataSource; | |
import java.sql.Connection; | |
import java.sql.PreparedStatement; | |
import java.sql.ResultSet; | |
import java.sql.SQLException; | |
import java.util.Collection; | |
import java.util.LinkedHashSet; | |
import java.util.Set; | |
/** | |
* <p> | |
* Realm that allows authentication and authorization via JDBC calls. The default queries suggest a potential schema | |
* for retrieving the user's password for authentication, and querying for a user's roles and permissions. The | |
* default queries can be overridden by setting the query properties of the realm. | |
* </p> | |
* | |
* <p> | |
* If the default implementation | |
* of authentication and authorization cannot handle your schema, this class can be subclassed and the | |
* appropriate methods overridden. (usually {@link #doGetAuthenticationInfo(org.jsecurity.authc.AuthenticationToken)}, | |
* {@link #getRoleNamesForUser(java.sql.Connection,String)}, and/or {@link #getPermissions(java.sql.Connection,String,java.util.Collection)} | |
* </p> | |
* | |
* <p> | |
* This realm supports caching by extending from {@link org.jsecurity.realm.AuthorizingRealm}. | |
* </p> | |
* | |
* @author Jeremy Haile | |
* @since 0.2 | |
*/ | |
public class JdbcRealm extends AuthorizingRealm { | |
//TODO - complete JavaDoc | |
/*-------------------------------------------- | |
| C O N S T A N T S | | |
============================================*/ | |
/** | |
* 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 the roles that apply to a user. | |
*/ | |
protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?"; | |
/** | |
* The default query used to retrieve permissions that apply to a particular role. | |
*/ | |
protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?"; | |
private static final Log log = LogFactory.getLog(JdbcRealm.class); | |
/*-------------------------------------------- | |
| I N S T A N C E V A R I A B L E S | | |
============================================*/ | |
protected DataSource dataSource; | |
protected String authenticationQuery = DEFAULT_AUTHENTICATION_QUERY; | |
protected String userRolesQuery = DEFAULT_USER_ROLES_QUERY; | |
protected String permissionsQuery = DEFAULT_PERMISSIONS_QUERY; | |
protected boolean permissionsLookupEnabled = false; | |
/*-------------------------------------------- | |
| C O N S T R U C T O R S | | |
============================================*/ | |
/*-------------------------------------------- | |
| 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. | |
* | |
* @param dataSource the SQL data source. | |
*/ | |
public void setDataSource(DataSource dataSource) { | |
this.dataSource = dataSource; | |
} | |
/** | |
* Overrides the default query used to retrieve a user's password during authentication. When using the default | |
* implementation, this query must take the user's username as a single parameter and return a single result | |
* with the user's password as the first column. If you require a solution that does not match this query | |
* structure, you can override {@link #doGetAuthenticationInfo(org.jsecurity.authc.AuthenticationToken)} or | |
* just {@link #getPasswordForUser(java.sql.Connection,String)} | |
* | |
* @param authenticationQuery the query to use for authentication. | |
* @see #DEFAULT_AUTHENTICATION_QUERY | |
*/ | |
public void setAuthenticationQuery(String authenticationQuery) { | |
this.authenticationQuery = authenticationQuery; | |
} | |
/** | |
* Overrides the default query used to retrieve a user's roles during authorization. When using the default | |
* implementation, this query must take the user's username as a single parameter and return a row | |
* per role with a single column containing the role name. If you require a solution that does not match this query | |
* structure, you can override {@link #doGetAuthorizationInfo(PrincipalCollection)} or just | |
* {@link #getRoleNamesForUser(java.sql.Connection,String)} | |
* | |
* @param userRolesQuery the query to use for retrieving a user's roles. | |
* @see #DEFAULT_USER_ROLES_QUERY | |
*/ | |
public void setUserRolesQuery(String userRolesQuery) { | |
this.userRolesQuery = userRolesQuery; | |
} | |
/** | |
* <p> | |
* Overrides the default query used to retrieve a user's permissions during authorization. When using the default | |
* implementation, this query must take a role name as the single parameter and return a row | |
* per permission with three columns containing the fully qualified name of the permission class, the permission | |
* name, and the permission actions (in that order). If you require a solution that does not match this query | |
* structure, you can override {@link #doGetAuthorizationInfo(PrincipalCollection)} or just | |
* {@link #getPermissions(java.sql.Connection,String,java.util.Collection)}</p> | |
* | |
* <p><b>Permissions are only retrieved if you set {@link #permissionsLookupEnabled} to true. Otherwise, | |
* this query is ignored.</b></p> | |
* | |
* @param permissionsQuery the query to use for retrieving permissions for a role. | |
* @see #DEFAULT_PERMISSIONS_QUERY | |
* @see #setPermissionsLookupEnabled(boolean) | |
*/ | |
public void setPermissionsQuery(String permissionsQuery) { | |
this.permissionsQuery = permissionsQuery; | |
} | |
/** | |
* Enables lookup of permissions during authorization. The default is "false" - meaning that only roles | |
* are associated with a user. Set this to true in order to lookup roles <b>and</b> permissions. | |
* | |
* @param permissionsLookupEnabled true if permissions should be looked up during authorization, or false if only | |
* roles should be looked up. | |
*/ | |
public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) { | |
this.permissionsLookupEnabled = permissionsLookupEnabled; | |
} | |
/*-------------------------------------------- | |
| M E T H O D S | | |
============================================*/ | |
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { | |
UsernamePasswordToken upToken = (UsernamePasswordToken) token; | |
String username = upToken.getUsername(); | |
// Null username is invalid | |
if (username == null) { | |
throw new AccountException("Null usernames are not allowed by this realm."); | |
} | |
Connection conn = null; | |
AuthenticationInfo info = null; | |
try { | |
conn = dataSource.getConnection(); | |
String password = getPasswordForUser(conn, username); | |
if (password == null) { | |
throw new UnknownAccountException("No account found for user [" + username + "]"); | |
} | |
info = buildAuthenticationInfo(username, password.toCharArray()); | |
} catch (SQLException e) { | |
final String message = "There was a SQL error while authenticating user [" + username + "]"; | |
if (log.isErrorEnabled()) { | |
log.error(message, e); | |
} | |
// Rethrow any SQL errors as an authentication exception | |
throw new AuthenticationException(message, e); | |
} finally { | |
JdbcUtils.closeConnection(conn); | |
} | |
return info; | |
} | |
protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) { | |
return new SimpleAuthenticationInfo(username, password, getName()); | |
} | |
private String getPasswordForUser(Connection conn, String username) throws SQLException { | |
PreparedStatement ps = null; | |
ResultSet rs = null; | |
String password = null; | |
try { | |
ps = conn.prepareStatement(authenticationQuery); | |
ps.setString(1, username); | |
// Execute query | |
rs = ps.executeQuery(); | |
// Loop over results - although we are only expecting one result, since usernames should be unique | |
boolean foundResult = false; | |
while (rs.next()) { | |
// Check to ensure only one row is processed | |
if (foundResult) { | |
throw new AuthenticationException("More than one user row found for user [" + username + "]. Usernames must be unique."); | |
} | |
password = rs.getString(1); | |
foundResult = true; | |
} | |
} finally { | |
JdbcUtils.closeResultSet(rs); | |
JdbcUtils.closeStatement(ps); | |
} | |
return password; | |
} | |
/** | |
* This implementation of the interface expects the principals collection to return a String username keyed off of | |
* this realm's {@link #getName() name} | |
* | |
* @see AuthorizingRealm#getAuthorizationInfo(org.jsecurity.subject.PrincipalCollection) | |
*/ | |
@Override | |
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { | |
//null usernames are invalid | |
if (principals == null) { | |
throw new AuthorizationException("PrincipalCollection method argument cannot be null."); | |
} | |
String username = (String) principals.fromRealm(getName()).iterator().next(); | |
Connection conn = null; | |
Set<String> roleNames = null; | |
Set<String> permissions = null; | |
try { | |
conn = dataSource.getConnection(); | |
// Retrieve roles and permissions from database | |
roleNames = getRoleNamesForUser(conn, username); | |
if( permissionsLookupEnabled ) { | |
permissions = getPermissions(conn, username, roleNames); | |
} | |
} catch (SQLException e) { | |
final String message = "There was a SQL error while authorizing user [" + username + "]"; | |
if (log.isErrorEnabled()) { | |
log.error(message, e); | |
} | |
// Rethrow any SQL errors as an authorization exception | |
throw new AuthorizationException(message, e); | |
} finally { | |
JdbcUtils.closeConnection(conn); | |
} | |
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames); | |
info.setStringPermissions( permissions ); | |
return info; | |
} | |
protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException { | |
PreparedStatement ps = null; | |
ResultSet rs = null; | |
Set<String> roleNames = new LinkedHashSet<String>(); | |
try { | |
ps = conn.prepareStatement(userRolesQuery); | |
ps.setString(1, username); | |
// Execute query | |
rs = ps.executeQuery(); | |
// Loop over results and add each returned role to a set | |
while (rs.next()) { | |
String roleName = rs.getString(1); | |
// Add the role to the list of names if it isn't null | |
if (roleName != null) { | |
roleNames.add(roleName); | |
} else { | |
if (log.isWarnEnabled()) { | |
log.warn("Null role name found while retrieving role names for user [" + username + "]"); | |
} | |
} | |
} | |
} finally { | |
JdbcUtils.closeResultSet(rs); | |
JdbcUtils.closeStatement(ps); | |
} | |
return roleNames; | |
} | |
protected Set<String> getPermissions(Connection conn, String username, Collection<String> roleNames) throws SQLException { | |
PreparedStatement ps = null; | |
ResultSet rs = null; | |
Set<String> permissions = new LinkedHashSet<String>(); | |
try { | |
for (String roleName : roleNames) { | |
ps = conn.prepareStatement(permissionsQuery); | |
ps.setString(1, roleName); | |
// Execute query | |
rs = ps.executeQuery(); | |
// Loop over results and add each returned role to a set | |
while (rs.next()) { | |
String permissionString = rs.getString(1); | |
// Add the permission to the set of permissions | |
permissions.add(permissionString); | |
} | |
} | |
} finally { | |
JdbcUtils.closeResultSet(rs); | |
JdbcUtils.closeStatement(ps); | |
} | |
return permissions; | |
} | |
} |