588 lines
17 KiB
Java
588 lines
17 KiB
Java
/*
|
|
* 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.catalina.realm;
|
|
|
|
|
|
import java.security.Principal;
|
|
import java.sql.Connection;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.ResultSet;
|
|
import java.sql.SQLException;
|
|
import java.util.ArrayList;
|
|
|
|
import javax.naming.Context;
|
|
import javax.sql.DataSource;
|
|
|
|
import org.apache.catalina.LifecycleException;
|
|
import org.apache.naming.ContextBindings;
|
|
|
|
/**
|
|
*
|
|
* Implementation of <b>Realm</b> that works with any JDBC JNDI DataSource.
|
|
* See the JDBCRealm.howto for more details on how to set up the database and
|
|
* for configuration options.
|
|
*
|
|
* @author Glenn L. Nielsen
|
|
* @author Craig R. McClanahan
|
|
* @author Carson McDonald
|
|
* @author Ignacio Ortega
|
|
*/
|
|
public class DataSourceRealm extends RealmBase {
|
|
|
|
|
|
// ----------------------------------------------------- Instance Variables
|
|
|
|
|
|
/**
|
|
* The generated string for the roles PreparedStatement
|
|
*/
|
|
private String preparedRoles = null;
|
|
|
|
|
|
/**
|
|
* The generated string for the credentials PreparedStatement
|
|
*/
|
|
private String preparedCredentials = null;
|
|
|
|
|
|
/**
|
|
* The name of the JNDI JDBC DataSource
|
|
*/
|
|
protected String dataSourceName = null;
|
|
|
|
|
|
/**
|
|
* Context local datasource.
|
|
*/
|
|
protected boolean localDataSource = false;
|
|
|
|
|
|
/**
|
|
* Descriptive information about this Realm implementation.
|
|
* @deprecated This will be removed in Tomcat 9 onwards.
|
|
*/
|
|
@Deprecated
|
|
protected static final String name = "DataSourceRealm";
|
|
|
|
|
|
/**
|
|
* The column in the user role table that names a role
|
|
*/
|
|
protected String roleNameCol = null;
|
|
|
|
|
|
/**
|
|
* The column in the user table that holds the user's credentials
|
|
*/
|
|
protected String userCredCol = null;
|
|
|
|
|
|
/**
|
|
* The column in the user table that holds the user's name
|
|
*/
|
|
protected String userNameCol = null;
|
|
|
|
|
|
/**
|
|
* The table that holds the relation between user's and roles
|
|
*/
|
|
protected String userRoleTable = null;
|
|
|
|
|
|
/**
|
|
* The table that holds user data.
|
|
*/
|
|
protected String userTable = null;
|
|
|
|
|
|
/**
|
|
* Last connection attempt.
|
|
*/
|
|
private volatile boolean connectionSuccess = true;
|
|
|
|
|
|
// ------------------------------------------------------------- Properties
|
|
|
|
|
|
/**
|
|
* @return the name of the JNDI JDBC DataSource.
|
|
*/
|
|
public String getDataSourceName() {
|
|
return dataSourceName;
|
|
}
|
|
|
|
/**
|
|
* Set the name of the JNDI JDBC DataSource.
|
|
*
|
|
* @param dataSourceName the name of the JNDI JDBC DataSource
|
|
*/
|
|
public void setDataSourceName( String dataSourceName) {
|
|
this.dataSourceName = dataSourceName;
|
|
}
|
|
|
|
/**
|
|
* @return if the datasource will be looked up in the webapp JNDI Context.
|
|
*/
|
|
public boolean getLocalDataSource() {
|
|
return localDataSource;
|
|
}
|
|
|
|
/**
|
|
* Set to true to cause the datasource to be looked up in the webapp JNDI
|
|
* Context.
|
|
*
|
|
* @param localDataSource the new flag value
|
|
*/
|
|
public void setLocalDataSource(boolean localDataSource) {
|
|
this.localDataSource = localDataSource;
|
|
}
|
|
|
|
/**
|
|
* @return the column in the user role table that names a role.
|
|
*/
|
|
public String getRoleNameCol() {
|
|
return roleNameCol;
|
|
}
|
|
|
|
/**
|
|
* Set the column in the user role table that names a role.
|
|
*
|
|
* @param roleNameCol The column name
|
|
*/
|
|
public void setRoleNameCol( String roleNameCol ) {
|
|
this.roleNameCol = roleNameCol;
|
|
}
|
|
|
|
/**
|
|
* @return the column in the user table that holds the user's credentials.
|
|
*/
|
|
public String getUserCredCol() {
|
|
return userCredCol;
|
|
}
|
|
|
|
/**
|
|
* Set the column in the user table that holds the user's credentials.
|
|
*
|
|
* @param userCredCol The column name
|
|
*/
|
|
public void setUserCredCol( String userCredCol ) {
|
|
this.userCredCol = userCredCol;
|
|
}
|
|
|
|
/**
|
|
* @return the column in the user table that holds the user's name.
|
|
*/
|
|
public String getUserNameCol() {
|
|
return userNameCol;
|
|
}
|
|
|
|
/**
|
|
* Set the column in the user table that holds the user's name.
|
|
*
|
|
* @param userNameCol The column name
|
|
*/
|
|
public void setUserNameCol( String userNameCol ) {
|
|
this.userNameCol = userNameCol;
|
|
}
|
|
|
|
/**
|
|
* @return the table that holds the relation between user's and roles.
|
|
*/
|
|
public String getUserRoleTable() {
|
|
return userRoleTable;
|
|
}
|
|
|
|
/**
|
|
* Set the table that holds the relation between user's and roles.
|
|
*
|
|
* @param userRoleTable The table name
|
|
*/
|
|
public void setUserRoleTable( String userRoleTable ) {
|
|
this.userRoleTable = userRoleTable;
|
|
}
|
|
|
|
/**
|
|
* @return the table that holds user data..
|
|
*/
|
|
public String getUserTable() {
|
|
return userTable;
|
|
}
|
|
|
|
/**
|
|
* Set the table that holds user data.
|
|
*
|
|
* @param userTable The table name
|
|
*/
|
|
public void setUserTable( String userTable ) {
|
|
this.userTable = userTable;
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------- Public Methods
|
|
|
|
/**
|
|
* Return the Principal associated with the specified username and
|
|
* credentials, if there is one; otherwise return <code>null</code>.
|
|
*
|
|
* If there are any errors with the JDBC connection, executing
|
|
* the query or anything we return null (don't authenticate). This
|
|
* event is also logged, and the connection will be closed so that
|
|
* a subsequent request will automatically re-open it.
|
|
*
|
|
* @param username Username of the Principal to look up
|
|
* @param credentials Password or other credentials to use in
|
|
* authenticating this username
|
|
* @return the associated principal, or <code>null</code> if there is none.
|
|
*/
|
|
@Override
|
|
public Principal authenticate(String username, String credentials) {
|
|
|
|
// No user or no credentials
|
|
// Can't possibly authenticate, don't bother the database then
|
|
if (username == null || credentials == null) {
|
|
return null;
|
|
}
|
|
|
|
Connection dbConnection = null;
|
|
|
|
// Ensure that we have an open database connection
|
|
dbConnection = open();
|
|
if (dbConnection == null) {
|
|
// If the db connection open fails, return "not authenticated"
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Acquire a Principal object for this user
|
|
return authenticate(dbConnection, username, credentials);
|
|
}
|
|
finally
|
|
{
|
|
close(dbConnection);
|
|
}
|
|
}
|
|
|
|
|
|
@Override
|
|
public boolean isAvailable() {
|
|
return connectionSuccess;
|
|
}
|
|
|
|
// -------------------------------------------------------- Package Methods
|
|
|
|
|
|
// ------------------------------------------------------ Protected Methods
|
|
|
|
|
|
/**
|
|
* Return the Principal associated with the specified username and
|
|
* credentials, if there is one; otherwise return <code>null</code>.
|
|
*
|
|
* @param dbConnection The database connection to be used
|
|
* @param username Username of the Principal to look up
|
|
* @param credentials Password or other credentials to use in
|
|
* authenticating this username
|
|
* @return the associated principal, or <code>null</code> if there is none.
|
|
*/
|
|
protected Principal authenticate(Connection dbConnection,
|
|
String username,
|
|
String credentials) {
|
|
// No user or no credentials
|
|
// Can't possibly authenticate, don't bother the database then
|
|
if (username == null || credentials == null) {
|
|
if (containerLog.isTraceEnabled())
|
|
containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure",
|
|
username));
|
|
return null;
|
|
}
|
|
|
|
// Look up the user's credentials
|
|
String dbCredentials = getPassword(dbConnection, username);
|
|
|
|
if(dbCredentials == null) {
|
|
// User was not found in the database.
|
|
// Waste a bit of time as not to reveal that the user does not exist.
|
|
getCredentialHandler().mutate(credentials);
|
|
|
|
if (containerLog.isTraceEnabled())
|
|
containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure",
|
|
username));
|
|
return null;
|
|
}
|
|
|
|
// Validate the user's credentials
|
|
boolean validated = getCredentialHandler().matches(credentials, dbCredentials);
|
|
|
|
if (validated) {
|
|
if (containerLog.isTraceEnabled())
|
|
containerLog.trace(sm.getString("dataSourceRealm.authenticateSuccess",
|
|
username));
|
|
} else {
|
|
if (containerLog.isTraceEnabled())
|
|
containerLog.trace(sm.getString("dataSourceRealm.authenticateFailure",
|
|
username));
|
|
return null;
|
|
}
|
|
|
|
ArrayList<String> list = getRoles(dbConnection, username);
|
|
|
|
// Create and return a suitable Principal for this user
|
|
return new GenericPrincipal(username, credentials, list);
|
|
}
|
|
|
|
|
|
/**
|
|
* Close the specified database connection.
|
|
*
|
|
* @param dbConnection The connection to be closed
|
|
*/
|
|
protected void close(Connection dbConnection) {
|
|
|
|
// Do nothing if the database connection is already closed
|
|
if (dbConnection == null)
|
|
return;
|
|
|
|
// Commit if not auto committed
|
|
try {
|
|
if (!dbConnection.getAutoCommit()) {
|
|
dbConnection.commit();
|
|
}
|
|
} catch (SQLException e) {
|
|
containerLog.error("Exception committing connection before closing:", e);
|
|
}
|
|
|
|
// Close this database connection, and log any errors
|
|
try {
|
|
dbConnection.close();
|
|
} catch (SQLException e) {
|
|
containerLog.error(sm.getString("dataSourceRealm.close"), e); // Just log it here
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Open the specified database connection.
|
|
*
|
|
* @return Connection to the database
|
|
*/
|
|
protected Connection open() {
|
|
|
|
try {
|
|
Context context = null;
|
|
if (localDataSource) {
|
|
context = ContextBindings.getClassLoader();
|
|
context = (Context) context.lookup("comp/env");
|
|
} else {
|
|
context = getServer().getGlobalNamingContext();
|
|
}
|
|
DataSource dataSource = (DataSource)context.lookup(dataSourceName);
|
|
Connection connection = dataSource.getConnection();
|
|
connectionSuccess = true;
|
|
return connection;
|
|
} catch (Exception e) {
|
|
connectionSuccess = false;
|
|
// Log the problem for posterity
|
|
containerLog.error(sm.getString("dataSourceRealm.exception"), e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
@Deprecated
|
|
protected String getName() {
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* @return the password associated with the given principal's user name.
|
|
*/
|
|
@Override
|
|
protected String getPassword(String username) {
|
|
|
|
Connection dbConnection = null;
|
|
|
|
// Ensure that we have an open database connection
|
|
dbConnection = open();
|
|
if (dbConnection == null) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return getPassword(dbConnection, username);
|
|
} finally {
|
|
close(dbConnection);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the password associated with the given principal's user name.
|
|
*
|
|
* @param dbConnection The database connection to be used
|
|
* @param username Username for which password should be retrieved
|
|
*
|
|
* @return the password for the specified user
|
|
*/
|
|
protected String getPassword(Connection dbConnection, String username) {
|
|
|
|
String dbCredentials = null;
|
|
|
|
try (PreparedStatement stmt = dbConnection.prepareStatement(preparedCredentials)) {
|
|
stmt.setString(1, username);
|
|
|
|
try (ResultSet rs = stmt.executeQuery()) {
|
|
if (rs.next()) {
|
|
dbCredentials = rs.getString(1);
|
|
}
|
|
|
|
return (dbCredentials != null) ? dbCredentials.trim() : null;
|
|
}
|
|
} catch (SQLException e) {
|
|
containerLog.error(sm.getString("dataSourceRealm.getPassword.exception", username), e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the Principal associated with the given user name.
|
|
* @param username the user name
|
|
* @return the principal object
|
|
*/
|
|
@Override
|
|
protected Principal getPrincipal(String username) {
|
|
Connection dbConnection = open();
|
|
if (dbConnection == null) {
|
|
return new GenericPrincipal(username, null, null);
|
|
}
|
|
try {
|
|
return new GenericPrincipal(username,
|
|
getPassword(dbConnection, username),
|
|
getRoles(dbConnection, username));
|
|
} finally {
|
|
close(dbConnection);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Return the roles associated with the given user name.
|
|
* @param username User name for which roles should be retrieved
|
|
* @return an array list of the role names
|
|
*/
|
|
protected ArrayList<String> getRoles(String username) {
|
|
|
|
Connection dbConnection = null;
|
|
|
|
// Ensure that we have an open database connection
|
|
dbConnection = open();
|
|
if (dbConnection == null) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return getRoles(dbConnection, username);
|
|
} finally {
|
|
close(dbConnection);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the roles associated with the given user name.
|
|
*
|
|
* @param dbConnection The database connection to be used
|
|
* @param username User name for which roles should be retrieved
|
|
*
|
|
* @return an array list of the role names
|
|
*/
|
|
protected ArrayList<String> getRoles(Connection dbConnection, String username) {
|
|
|
|
if (allRolesMode != AllRolesMode.STRICT_MODE && !isRoleStoreDefined()) {
|
|
// Using an authentication only configuration and no role store has
|
|
// been defined so don't spend cycles looking
|
|
return null;
|
|
}
|
|
|
|
ArrayList<String> list = null;
|
|
|
|
try (PreparedStatement stmt = dbConnection.prepareStatement(preparedRoles)) {
|
|
stmt.setString(1, username);
|
|
|
|
try (ResultSet rs = stmt.executeQuery()) {
|
|
list = new ArrayList<>();
|
|
|
|
while (rs.next()) {
|
|
String role = rs.getString(1);
|
|
if (role != null) {
|
|
list.add(role.trim());
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
} catch(SQLException e) {
|
|
containerLog.error(sm.getString("dataSourceRealm.getRoles.exception", username), e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
private boolean isRoleStoreDefined() {
|
|
return userRoleTable != null || roleNameCol != null;
|
|
}
|
|
|
|
|
|
// ------------------------------------------------------ Lifecycle Methods
|
|
|
|
/**
|
|
* Prepare for the beginning of active use of the public methods of this
|
|
* component and implement the requirements of
|
|
* {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
|
|
*
|
|
* @exception LifecycleException if this component detects a fatal error
|
|
* that prevents this component from being used
|
|
*/
|
|
@Override
|
|
protected void startInternal() throws LifecycleException {
|
|
|
|
// Create the roles PreparedStatement string
|
|
StringBuilder temp = new StringBuilder("SELECT ");
|
|
temp.append(roleNameCol);
|
|
temp.append(" FROM ");
|
|
temp.append(userRoleTable);
|
|
temp.append(" WHERE ");
|
|
temp.append(userNameCol);
|
|
temp.append(" = ?");
|
|
preparedRoles = temp.toString();
|
|
|
|
// Create the credentials PreparedStatement string
|
|
temp = new StringBuilder("SELECT ");
|
|
temp.append(userCredCol);
|
|
temp.append(" FROM ");
|
|
temp.append(userTable);
|
|
temp.append(" WHERE ");
|
|
temp.append(userNameCol);
|
|
temp.append(" = ?");
|
|
preparedCredentials = temp.toString();
|
|
|
|
super.startInternal();
|
|
}
|
|
}
|