/* * 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.CallableStatement; 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 Realm that works with JDBC JNDI DataSources * that are accessed via stored procedures. * See the DataSourceViaSPRealm.howto for more details on how to set up the database and * for configuration options. * * @author Eugene Adell */ public class DataSourceViaSPRealm extends RealmBase { // ----------------------------------------------------- Instance Variables /** * The generated string for the roles CallableStatement */ private String callableRoles = null; /** * The generated string for the credentials CallableStatement */ private String callableCredentials = null; /** * The name of the JNDI JDBC DataSource */ protected String dataSourceName = null; /** * Context local datasource. */ protected boolean localDataSource = false; /** * The column in the user table that holds the user's credentials */ protected String userProcStoc = null; /** * The column in the user table that holds the user's credentials */ protected String roleProcStoc = 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 table that holds the user's credentials. */ public String getUserProcStoc() { return userProcStoc; } /** * @return the column in the user table that holds the user's credentials. */ public String getRoleProcStoc() { return roleProcStoc; } /** * Set the column in the user table that holds the user's credentials. * * @param userCredCol The column name */ public void setUserProcStoc( String userProcStoc ) { this.userProcStoc = userProcStoc; } /** * Set the column in the user table that holds the user's credentials. * * @param userCredCol The column name */ public void setRoleProcStoc( String roleProcStoc ) { this.roleProcStoc = roleProcStoc; } // --------------------------------------------------------- Public Methods /** * Return the Principal associated with the specified username and * credentials, if there is one; otherwise return null. * * 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 null 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 null. * * @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 null 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("dataSourceViaSPRealm.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("dataSourceViaSPRealm.authenticateFailure", username)); return null; } // Validate the user's credentials boolean validated = getCredentialHandler().matches(credentials, dbCredentials); if (validated) { if (containerLog.isTraceEnabled()) containerLog.trace(sm.getString("dataSourceViaSPRealm.authenticateSuccess", username)); } else { if (containerLog.isTraceEnabled()) containerLog.trace(sm.getString("dataSourceViaSPRealm.authenticateFailure", username)); return null; } ArrayList 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("dataSourceViaSPRealm.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("dataSourceViaSPRealm.exception"), e); } return null; } /** * @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 (CallableStatement stmt = dbConnection.prepareCall(callableCredentials)) { 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("dataSourceViaSPRealm.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 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 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 list = null; try (CallableStatement stmt = dbConnection.prepareCall(callableRoles)) { 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("dataSourceViaSPRealm.getRoles.exception", username), e); } return null; } private boolean isRoleStoreDefined() { return roleProcStoc != 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 { // Prepare the CallableStatement strings StringBuilder temp = new StringBuilder(32); // Create the credentials CallableStatement string temp.append("call "); temp.append(userProcStoc); temp.append("(?)"); callableCredentials = temp.toString(); // Create the roles CallableStatement string temp.delete(5, temp.length()); temp.append(roleProcStoc); temp.append("(?)"); callableRoles = temp.toString(); super.startInternal(); } }