diff --git a/java/org/apache/catalina/realm/DataSourceRealm.java b/java/org/apache/catalina/realm/DataSourceRealm.java
index 5adf14f615fd..58d91f774dfb 100644
--- a/java/org/apache/catalina/realm/DataSourceRealm.java
+++ b/java/org/apache/catalina/realm/DataSourceRealm.java
@@ -18,16 +18,25 @@
import java.security.Principal;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
import java.sql.SQLException;
+import java.sql.Types;
import java.util.ArrayList;
-
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
import javax.naming.Context;
import javax.sql.DataSource;
import org.apache.catalina.LifecycleException;
+import org.apache.catalina.TomcatPrincipal;
import org.apache.naming.ContextBindings;
/**
@@ -43,6 +52,16 @@
*/
public class DataSourceRealm extends RealmBase {
+ /**
+ * String object (empty string) signaling an empty list of user attributes, that
+ * is, no valid or existing user attributes to additionally query from the user
+ * table have been specified.
+ *
+ * Will be assigned to userAttributesStatement in order to prevent
+ * entering the DCL lock in method getUserAttributesStatement every
+ * time in case there are no user attributes to query.
+ */
+ private static final String USER_ATTRIBUTES_NONE_REQUESTED = new String();
// ----------------------------------------------------- Instance Variables
@@ -59,6 +78,18 @@ public class DataSourceRealm extends RealmBase {
private String preparedCredentials = null;
+ /**
+ * The generated string for the user attributes PreparedStatement
+ */
+ private String preparedAttributesTail = null;
+
+
+ /**
+ * The generated string for the user attributes available PreparedStatement
+ */
+ private String preparedAttributesAvailable = null;
+
+
/**
* The name of the JNDI JDBC DataSource
*/
@@ -107,6 +138,22 @@ public class DataSourceRealm extends RealmBase {
private volatile boolean connectionSuccess = true;
+ /**
+ * The comma separated names of user attributes to additionally query from the
+ * user table. These will be provided to the user through the created
+ * Principal's attributes map.
+ */
+ protected String userAttributes;
+
+
+ /**
+ * Generated SQL statement to query additional user attributes from the user
+ * table.
+ */
+ private volatile String userAttributesStatement;
+ private final Object userAttributesStatementLock = new Object();
+
+
// ------------------------------------------------------------- Properties
@@ -223,6 +270,34 @@ public void setUserTable( String userTable ) {
this.userTable = userTable;
}
+ /**
+ * @return the comma separated names of user attributes to additionally query
+ * from the user table
+ */
+ public String getUserAttributes() {
+ return userAttributes;
+ }
+
+ /**
+ * Set the comma separated names of user attributes to additionally query from
+ * the user table. These will be provided to the user through the created
+ * Principal's attributes map. In this map, each field value is bound to
+ * the field's name, that is, the name of the field serves as the key of the
+ * mapping.
+ *
+ * If set to the wildcard character, or, if the wildcard character is part of
+ * the comma separated list, all available attributes - except the
+ * password attribute (as specified by userCredCol) - are
+ * queried. The wildcard character is defined by constant
+ * {@link RealmBase#USER_ATTRIBUTES_WILDCARD}. It defaults to the asterisk (*)
+ * character.
+ *
+ * @param userAttributes the comma separated names of user attributes
+ */
+ public void setUserAttributes(String userAttributes) {
+ this.userAttributes = userAttributes;
+ }
+
// --------------------------------------------------------- Public Methods
@@ -336,9 +411,10 @@ protected Principal authenticate(Connection dbConnection,
}
ArrayList list = getRoles(dbConnection, username);
+ Map attrs = getUserAttributesMap(dbConnection, username);
// Create and return a suitable Principal for this user
- return new GenericPrincipal(username, list);
+ return new GenericPrincipal(username, list, null, null, null, attrs);
}
@@ -463,8 +539,8 @@ protected Principal getPrincipal(String username) {
return new GenericPrincipal(username, null);
}
try {
- return new GenericPrincipal(username,
- getRoles(dbConnection, username));
+ return new GenericPrincipal(username, getRoles(dbConnection, username), null, null,
+ null, getUserAttributesMap(dbConnection, username));
} finally {
close(dbConnection);
}
@@ -539,6 +615,265 @@ private boolean isRoleStoreDefined() {
}
+ /**
+ * Return the specified user's requested user attributes as a map.
+ *
+ * This method does not support values of every possible SQL data type. Uses
+ * {@link ResultSet#getObject(int)} to get the attribute's value, except for
+ * columns of these SQL types:
+ *
+ *
{@link Types#ARRAY}
+ *
{@link Types#BLOB}
+ *
{@link Types#CLOB}
+ *
{@link Types#NCLOB}
+ *
+ * Other multivalued or complex types obtained from getObject(int)
+ * may not be serializable and so, cannot be defensively copied when being
+ * returned from {@link TomcatPrincipal#getAttribute(String)}. In that case,
+ * only a String with the object's string representation will be
+ * returned (in contrast to the object itself).
+ *
+ * In other words, user attributes queried by DataSourceRealm works
+ * well with values of a simple scalar type as well as for SQL arrays,
+ * BLOBs, CLOBs and NCLOBs. Any other types are not fully supported.
+ *
+ * @param dbConnection The database connection to be used
+ * @param username User name for which to return user attributes
+ *
+ * @return a map containing the specified user's requested user attributes
+ */
+ protected Map getUserAttributesMap(Connection dbConnection, String username) {
+
+ String preparedAttributes = getUserAttributesStatement(dbConnection);
+ if (preparedAttributes == null || preparedAttributes.isEmpty()) {
+ // Return null if the SQL statement was not yet built successfully ( = null), or
+ // if no user attributes have been requested (isEmpty).
+ return null;
+ }
+
+ try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAttributes)) {
+ stmt.setString(1, username);
+
+ try (ResultSet rs = stmt.executeQuery()) {
+
+ if (rs.next()) {
+ Map attrs = new LinkedHashMap<>();
+ ResultSetMetaData md = rs.getMetaData();
+ int ncols = md.getColumnCount();
+ for (int columnIndex = 1; columnIndex <= ncols; columnIndex++) {
+
+ String columnName = md.getColumnName(columnIndex);
+ // Ignore case, database may have case-insensitive field names
+ if (columnName.equalsIgnoreCase(userCredCol)) {
+ // Always skip userCredCol (must be there if all columns
+ // have been requested)
+ continue;
+ }
+
+ switch (md.getColumnType(columnIndex)) {
+ case Types.BLOB:
+ Blob blob = rs.getBlob(columnIndex);
+ if (blob != null) {
+ attrs.put(columnName, blob.getBytes(1, (int) blob.length()));
+ blob.free();
+ } else {
+ attrs.put(columnName, null);
+ }
+ break;
+
+ case Types.CLOB:
+ case Types.NCLOB:
+ Clob clob = rs.getClob(columnIndex);
+ if (clob != null) {
+ attrs.put(columnName, clob.getSubString(1, (int) clob.length()));
+ clob.free();
+ } else {
+ attrs.put(columnName, null);
+ }
+ break;
+
+ case Types.ARRAY:
+ Array array = rs.getArray(columnIndex);
+ if (array != null) {
+ attrs.put(columnName, array.getArray());
+ array.free();
+ } else {
+ attrs.put(columnName, null);
+ }
+ break;
+
+ default:
+ attrs.put(columnName, rs.getObject(columnIndex));
+ break;
+ }
+ }
+
+ return attrs.size() > 0 ? attrs : null;
+ }
+ }
+ } catch (SQLException e) {
+ containerLog.error(
+ sm.getString("dataSourceRealm.getUserAttributes.exception", username), e);
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Return the SQL statement for querying additional user attributes. The
+ * statement is lazily initialized (lazily initialized singleton with
+ * double-checked locking, DCL) since building it may require an extra
+ * database query under some conditions.
+ *
+ * @param dbConnection connection for accessing the database
+ */
+ private String getUserAttributesStatement(Connection dbConnection) {
+ // DCL so userAttributesStatement MUST be volatile
+ if (userAttributesStatement == null) {
+ synchronized (userAttributesStatementLock) {
+ if (userAttributesStatement == null) {
+ List requestedAttributes = parseUserAttributes(userAttributes);
+ if (requestedAttributes == null) {
+ // No user attributes have been specified. Do not query any extra attributes.
+ // Set field to (non-null) empty string USER_ATTRIBUTES_NONE_REQUESTED, so
+ // we don't try to enter the critical section next time this method is
+ // called.
+ userAttributesStatement = USER_ATTRIBUTES_NONE_REQUESTED;
+ return userAttributesStatement;
+ }
+ if (requestedAttributes.size() == 1
+ && requestedAttributes.get(0).equals(USER_ATTRIBUTES_WILDCARD)) {
+ // wildcard case
+ userAttributesStatement = "SELECT *" + preparedAttributesTail;
+ return userAttributesStatement;
+ }
+ List availableUserAttributes = getAvailableUserAttributes(dbConnection);
+ if (availableUserAttributes == null) {
+ // Failed getting all available user attributes (this has already been
+ // logged) so, just return, leaving userAttributesStatement null (aka
+ // uninitialized, will try again next time).
+ return null;
+ }
+ requestedAttributes =
+ validateUserAttributes(requestedAttributes, availableUserAttributes);
+ if (requestedAttributes == null) {
+ // None of the requested user attributes are actually available or valid. Do
+ // not query any attributes.
+ // Set field to (non-null) empty string USER_ATTRIBUTES_NONE_REQUESTED, so
+ // we don't try to enter the critical section next time this method is
+ // called.
+ userAttributesStatement = USER_ATTRIBUTES_NONE_REQUESTED;
+ return userAttributesStatement;
+ }
+ StringBuilder sb = new StringBuilder("SELECT ");
+ boolean first = true;
+ for (String attr : requestedAttributes) {
+ if (first) {
+ first = false;
+ } else {
+ sb.append(", ");
+ }
+ sb.append(attr);
+ }
+ userAttributesStatement = sb.append(preparedAttributesTail).toString();
+ }
+ }
+ }
+ return userAttributesStatement;
+ }
+
+
+ /**
+ * Return a list of all available user attributes. The list contains all field
+ * names of the user table, except for fields for which access is denied (e. g.
+ * field userCredCol).
+ *
+ * @param dbConnection connection for accessing the database
+ */
+ private List getAvailableUserAttributes(Connection dbConnection) {
+
+ try (PreparedStatement stmt =
+ dbConnection.prepareStatement(preparedAttributesAvailable)) {
+
+ try (ResultSet rs = stmt.executeQuery()) {
+
+ // Query is not expected to return any rows (...WHERE 1 = 2) so, must not call
+ // next(). ResultSetMetadata is available before calling next() anyway.
+ List result = new ArrayList<>();
+ ResultSetMetaData md = rs.getMetaData();
+ int ncols = md.getColumnCount();
+ for (int columnIndex = 1; columnIndex <= ncols; columnIndex++) {
+ String columnName = md.getColumnName(columnIndex);
+ // Ignore case, database may have case-insensitive field names
+ if (columnName.equalsIgnoreCase(userCredCol)) {
+ // always skip userCredCol
+ continue;
+ }
+ result.add(columnName);
+ }
+ return result;
+ }
+ } catch (SQLException e) {
+ containerLog.error(sm.getString(
+ "dataSourceRealm.getAvailableUserAttributes.exception"), e);
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Validate the specified list of attribute names and return a list containing
+ * valid items only or null, if there are no valid attributes.
+ *
+ * If availableAttributes is not null, log an
+ * userAttributeNotFound warning message for each specified attribute
+ * not contained in that list.
+ *
+ * If userCredCol is not null, and not the empty
+ * string, log an userAttributeAccessDenied warning message for each
+ * specified attribute equal to that value.
+ *
+ * @param userAttributes list of attribute names to validate
+ * @param availableAttributes list of available (aka valid) attribute names
+ * @return the validated attribute names as a list or null, if
+ * there are no valid attributes
+ */
+ private List validateUserAttributes(List userAttributes,
+ List availableAttributes) {
+ if (userAttributes == null || userAttributes.size() == 0) {
+ return null;
+ }
+ if (userAttributes.size() == 1 && userAttributes.get(0).equals(USER_ATTRIBUTES_WILDCARD)) {
+ return userAttributes;
+ }
+ String deniedAttribute = userCredCol;
+ if (deniedAttribute != null && deniedAttribute.isEmpty()) {
+ deniedAttribute = null;
+ }
+ List attrs = new ArrayList<>();
+ for (String name : userAttributes) {
+ if (deniedAttribute != null && deniedAttribute.equals(name)) {
+ if (containerLog.isWarnEnabled()) {
+ containerLog
+ .warn(sm.getString("dataSourceRealm.userAttributeAccessDenied", name));
+ }
+ continue;
+ } else if (availableAttributes != null && !availableAttributes.contains(name)) {
+ if (containerLog.isWarnEnabled()) {
+ containerLog.warn(sm.getString("dataSourceRealm.userAttributeNotFound", name));
+ }
+ continue;
+ } else if (name.equals(USER_ATTRIBUTES_WILDCARD)) {
+ return Collections.singletonList(USER_ATTRIBUTES_WILDCARD);
+ }
+ attrs.add(name);
+ }
+ return attrs.size() > 0 ? attrs : null;
+ }
+
+
// ------------------------------------------------------ Lifecycle Methods
/**
@@ -572,6 +907,26 @@ protected void startInternal() throws LifecycleException {
temp.append(" = ?");
preparedCredentials = temp.toString();
+ // Create the user attributes PreparedStatement string (only its tail w/o SELECT
+ // clause)
+ temp = new StringBuilder(" FROM ");
+ temp.append(userTable);
+ temp.append(" WHERE ");
+ temp.append(userNameCol);
+ temp.append(" = ?");
+ preparedAttributesTail = temp.toString();
+
+ // Create the available user attributes PreparedStatement string
+ // With this statement, we only want to query the definitions of all fields of
+ // the user table. In other words, we want an empty ResultSet, which, however,
+ // still has its ResultSetMetadata describing all the column types. In order to
+ // prevent the database from sending any row, it uses a WHERE clause that always
+ // evaluates to false (WHERE 1 = 2).
+ temp = new StringBuilder("SELECT * FROM ");
+ temp.append(userTable);
+ temp.append(" WHERE 1 = 2");
+ preparedAttributesAvailable = temp.toString();
+
super.startInternal();
}
}
diff --git a/java/org/apache/catalina/realm/LocalStrings.properties b/java/org/apache/catalina/realm/LocalStrings.properties
index cd09ed4b50f2..60297175dc72 100644
--- a/java/org/apache/catalina/realm/LocalStrings.properties
+++ b/java/org/apache/catalina/realm/LocalStrings.properties
@@ -32,6 +32,10 @@ dataSourceRealm.commit=Exception committing connection before closing
dataSourceRealm.exception=Exception performing authentication
dataSourceRealm.getPassword.exception=Exception retrieving password for [{0}]
dataSourceRealm.getRoles.exception=Exception retrieving roles for [{0}]
+dataSourceRealm.getUserAttributes.exception=Exception retrieving user attributes for [{0}]
+dataSourceRealm.getAvailableUserAttributes.exception=Exception retrieving names of all available user attributes
+dataSourceRealm.userAttributeNotFound=Specified user attribute [{0}] does not exist
+dataSourceRealm.userAttributeAccessDenied=Access to specified user attribute [{0}] has been denied
jaasCallback.username=Returned username [{0}]
diff --git a/java/org/apache/catalina/realm/RealmBase.java b/java/org/apache/catalina/realm/RealmBase.java
index f3598e5cfc0a..270911d4b9bd 100644
--- a/java/org/apache/catalina/realm/RealmBase.java
+++ b/java/org/apache/catalina/realm/RealmBase.java
@@ -26,6 +26,7 @@
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -76,6 +77,21 @@ public abstract class RealmBase extends LifecycleMBeanBase implements Realm {
private static final List> credentialHandlerClasses =
new ArrayList<>();
+ /**
+ * The character used for delimiting user attribute names.
+ *
+ * Applies to some of the Realm implementations only.
+ */
+ protected static final String USER_ATTRIBUTES_DELIMITER = ",";
+
+ /**
+ * The character used as wildcard in user attribute lists. Using it means
+ * query all available user attributes.
+ *
+ * Applies to some of the Realm implementations only.
+ */
+ protected static final String USER_ATTRIBUTES_WILDCARD = "*";
+
static {
// Order is important since it determines the search order for a
// matching handler if only an algorithm is specified when calling
@@ -1293,6 +1309,40 @@ protected Server getServer() {
}
+ /**
+ * Parse the specified delimiter separated attribute names and return a list of
+ * that names or null, if no attributes have been specified.
+ *
+ * If a wildcard character is found, return a list consisting of a single
+ * wildcard character only.
+ *
+ * @param userAttributes comma separated names of attributes to parse
+ * @return a list containing the parsed attribute names or null, if
+ * no attributes have been specified
+ */
+ protected List parseUserAttributes(String userAttributes) {
+ if (userAttributes == null) {
+ return null;
+ }
+ List attrs = new ArrayList<>();
+ for (String name : userAttributes.split(USER_ATTRIBUTES_DELIMITER)) {
+ name = name.trim();
+ if (name.length() == 0) {
+ continue;
+ }
+ if (name.equals(USER_ATTRIBUTES_WILDCARD)) {
+ return Collections.singletonList(USER_ATTRIBUTES_WILDCARD);
+ }
+ if (attrs.contains(name)) {
+ // skip duplicates
+ continue;
+ }
+ attrs.add(name);
+ }
+ return attrs.size() > 0 ? attrs : null;
+ }
+
+
// --------------------------------------------------------- Static Methods
/**
diff --git a/java/org/apache/catalina/realm/mbeans-descriptors.xml b/java/org/apache/catalina/realm/mbeans-descriptors.xml
index 9885e740ad70..f5d1520e863f 100644
--- a/java/org/apache/catalina/realm/mbeans-descriptors.xml
+++ b/java/org/apache/catalina/realm/mbeans-descriptors.xml
@@ -57,6 +57,10 @@
type="java.lang.String"
writeable="false"/>
+
+
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index bd2a1246f8b4..c37278b7a9ac 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -128,6 +128,10 @@
TomcatPrincipal and GenericPrincipal.
Patch provided by Carsten Klein. (michaelo)
+
+ xxx: Add support for additional user attributes to
+ DataSourceRealm. Patch provided by Carsten Klein. (michaelo)
+ 469: Include the Jakarata Annotations API in the classes that
Tomcat will not load from web applications. Pull request provided by
diff --git a/webapps/docs/config/realm.xml b/webapps/docs/config/realm.xml
index 78b4c20fd58b..a657267f3cb7 100644
--- a/webapps/docs/config/realm.xml
+++ b/webapps/docs/config/realm.xml
@@ -166,6 +166,34 @@
name. If not specified, the default is true.
+
+
Comma separated list of names of columns to additionally query from
+ the "users" table named by the userTable attribute. These
+ are provided to the application as Additional User Attributes
+ through the Principal's attributes map (with configured column names
+ used as the keys). This attribute also supports the wildcard character
+ *, which means to query all columns (except, for
+ security reasons, the column containing the user's credentials).
+
Any of the "users" table's columns can be specified, except the
+ column containing the user's credentials (i.e. password) named by the
+ userCredCol attribute. If the credentials column is
+ explicitly specified, it will be ignored and a warning will be logged.
+
+
Specified columns that are not present in the "users" table are
+ dropped from the list and a warning is logged, one for each missing
+ column. However, these checks are performed only once while the Realm
+ processes its first login attempt. If the structure of the "users"
+ table significantly changes after that point in time (i.e. a specified
+ column gets removed), a logged exception is likely the result for each
+ subsequent login attempt until the Realm gets restarted. Even so,
+ exceptions caused by user attribute queries do not prevent the user
+ from loggin in, but only cause the associated Principal's attributes
+ map being empty.
Name of the column, in the "users" table, which contains
the user's credentials (i.e. password). If a
@@ -173,6 +201,8 @@
will assume that the passwords have been encoded with the
specified algorithm. Otherwise, they will be assumed to be
in clear text.
+
For security reasons, the column named by this attribute cannot
+ be used as an Additional User Attribute.
@@ -192,7 +222,10 @@
Name of the "users" table, which must contain columns named
by the userNameCol and userCredCol
- attributes.
+ attributes. More columns containing additional user information could
+ freely be added. These can be provided to the application as
+ Additional User Attributes configured by the
+ userAttributes attribute.
diff --git a/webapps/docs/realm-howto.xml b/webapps/docs/realm-howto.xml
index d03291c4f26f..a45ab6c4ed9f 100644
--- a/webapps/docs/realm-howto.xml
+++ b/webapps/docs/realm-howto.xml
@@ -253,6 +253,95 @@ simplified to {digest}.
+
+
+
Some of the Realm implementations, currently only DataSourceRealm,
+support a feature called Additional User Attributes. With that, the
+Realm additionally queries a configurable list of attributes from the user's
+entry in the associated user database. The purpose of this feature is to provide
+extra user-related information, like user dispay name, e-mail address, department,
+room number, phone number etc., to the application with only a configured list of
+attribute names (like the SELECT clause of an SQL statement).
+
+
Additional user attributes are provided to the application through the
+Principal instance, which is associated with each authenticated
+request. Principals created by the DataSourceRealm maintain a set of
+read-only named attributes, accessible by these methods:
+
+
+
+This set of attributes is populated by the Realm with the queried additional
+user attributes. The configured names of the attributes to query additionally
+also serve as the names of the attributes. These are the real names of the user
+table's columns in the JDBC database.
+
+
The Principal returned for a request by method
+HttpServletRequest.getUserPrincipal() is of type
+java.security.Principal. The methods to access the Principal's
+attributes, however, are actually declared in the
+org.apache.catalina.TomcatPrincipal interface, which extends
+java.security.Principal and is the base type for all Principal
+instances used in Tomcat. So, in order to actually access the Principal's
+attributes, an instanceof check and casting is required. Have a
+look at this simple example, which dumps all available additional user
+attributes:
+
+
+
+The JSP example application referred to in the next section also lists the
+Principal's attributes. However, in order to get any user attributes, ensure
+to use DataSourceRealm
+with the example application and configure its userAttributes
+attribute accordingly:
+
+
An example SQL script to create an attributes-extended user table might
+look something like this (adapt the syntax as required for your particular
+database):
+
+
+
With that user table, the Realm's userAttributes attribute could
+look like:
+
+
+
Security alert: Please note, that this feature, when
+configured inappropriately and, depending on the user related data available in
+the Realm's user database, could leak sensitive user data.
+
An example SQL script to create the needed tables might look something
-like this (adapt the syntax as required for your particular database):
+like this (adapt the syntax as required for your particular database). You may
+add more columns to the "users" table to be provided as
+Additional User Attributes: