You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2021/09/01 11:47:46 UTC

[tomcat] branch 9.0.x updated (475e001 -> a63ed57)

This is an automated email from the ASF dual-hosted git repository.

remm pushed a change to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git.


    from 475e001  Update Commons Codec to 1.16-SNAPSHOT (2021-01-09)
     new 861b465  Improve the reusability of UserDatabase code
     new 75cdff2  Fix Javadoc warning
     new 1e50c19  Add available flag for UserDatabase
     new 346114b  Add a UserDatabase implementation based on DataSourceRealm
     new bbab0a6  Fix compile
     new 9eff810  Add lock use similar to the memory user database
     new 47b97e0  Add UserDatabase documentation
     new 2996ab0  Refactor to avoid NPE warnings in IDE
     new ce75358  Fix IDE warnings. Use <> where possible.
     new 5335d25  Add hashCode implementations that are aligned with existing equals()
     new 73b125b  Add Derby for the testsuite
     new a25cb20  Fix group roles not working
     new 7375b36  Add first pass at tests for the DataSource UserDatabase
     new e1d0a7d  Use Derby 10.14 for Java 8 compatibility
     new e3cf4ac  Checkstyle
     new a63ed57  Fix bad merge

The 16 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 build.properties.default                           |   19 +
 build.xml                                          |   20 +
 java/org/apache/catalina/UserDatabase.java         |   49 +
 .../mbeans/DataSourceUserDatabaseMBean.java        |  360 +++++
 .../mbeans/GlobalResourcesLifecycleListener.java   |    5 +
 .../apache/catalina/mbeans/LocalStrings.properties |    1 +
 java/org/apache/catalina/mbeans/MBeanUtils.java    |   30 +
 .../catalina/mbeans/MemoryUserDatabaseMBean.java   |  277 +---
 ...baseMBean.java => SparseUserDatabaseMBean.java} |   77 +-
 .../apache/catalina/realm/UserDatabaseRealm.java   |   69 +-
 .../catalina/users/DataSourceUserDatabase.java     | 1507 ++++++++++++++++++++
 ...ory.java => DataSourceUserDatabaseFactory.java} |   86 +-
 .../users/{MemoryGroup.java => GenericGroup.java}  |   74 +-
 .../users/{MemoryRole.java => GenericRole.java}    |   62 +-
 .../users/{MemoryUser.java => GenericUser.java}    |  144 +-
 .../apache/catalina/users/LocalStrings.properties  |    2 +
 java/org/apache/catalina/users/MemoryGroup.java    |  116 +-
 java/org/apache/catalina/users/MemoryRole.java     |   36 +-
 java/org/apache/catalina/users/MemoryUser.java     |  158 +-
 .../apache/catalina/users/SparseUserDatabase.java  |   13 +-
 .../apache/catalina/users/mbeans-descriptors.xml   |  346 +++++
 .../users/DataSourceUserDatabaseTests.java         |  228 +++
 webapps/docs/changelog.xml                         |   19 +
 webapps/docs/config/realm.xml                      |    8 +
 webapps/docs/jndi-resources-howto.xml              |  204 ++-
 25 files changed, 3169 insertions(+), 741 deletions(-)
 create mode 100644 java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java
 copy java/org/apache/catalina/mbeans/{MemoryUserDatabaseMBean.java => SparseUserDatabaseMBean.java} (79%)
 create mode 100644 java/org/apache/catalina/users/DataSourceUserDatabase.java
 copy java/org/apache/catalina/users/{MemoryUserDatabaseFactory.java => DataSourceUserDatabaseFactory.java} (57%)
 copy java/org/apache/catalina/users/{MemoryGroup.java => GenericGroup.java} (69%)
 copy java/org/apache/catalina/users/{MemoryRole.java => GenericRole.java} (57%)
 copy java/org/apache/catalina/users/{MemoryUser.java => GenericUser.java} (62%)
 copy test/org/apache/catalina/loader/TesterNeverWeavedClass.java => java/org/apache/catalina/users/SparseUserDatabase.java (79%)
 create mode 100644 test/org/apache/catalina/users/DataSourceUserDatabaseTests.java

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 02/16: Fix Javadoc warning

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 75cdff2a9d37d1c6b691def42ef41d433dd8d6dd
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Aug 17 15:54:42 2021 +0100

    Fix Javadoc warning
---
 java/org/apache/catalina/users/GenericGroup.java | 2 ++
 java/org/apache/catalina/users/GenericRole.java  | 2 ++
 java/org/apache/catalina/users/GenericUser.java  | 2 ++
 3 files changed, 6 insertions(+)

diff --git a/java/org/apache/catalina/users/GenericGroup.java b/java/org/apache/catalina/users/GenericGroup.java
index d59e57d..986c9f7 100644
--- a/java/org/apache/catalina/users/GenericGroup.java
+++ b/java/org/apache/catalina/users/GenericGroup.java
@@ -31,6 +31,8 @@ import org.apache.catalina.UserDatabase;
  * <p>Concrete implementation of {@link org.apache.catalina.Group} for a
  * {@link UserDatabase}.</p>
  *
+ * @param <UD> The specific type of UserDase with which this group is associated
+ *
  * @author Craig R. McClanahan
  */
 public class GenericGroup<UD extends UserDatabase> extends AbstractGroup {
diff --git a/java/org/apache/catalina/users/GenericRole.java b/java/org/apache/catalina/users/GenericRole.java
index c6b0eba..2957280 100644
--- a/java/org/apache/catalina/users/GenericRole.java
+++ b/java/org/apache/catalina/users/GenericRole.java
@@ -24,6 +24,8 @@ import org.apache.catalina.UserDatabase;
  * <p>Concrete implementation of {@link org.apache.catalina.Role} for a
  * {@link UserDatabase}.</p>
  *
+ * @param <UD> The specific type of UserDase with which this role is associated
+ *
  * @author Craig R. McClanahan
  */
 public class GenericRole<UD extends UserDatabase> extends AbstractRole {
diff --git a/java/org/apache/catalina/users/GenericUser.java b/java/org/apache/catalina/users/GenericUser.java
index 77b70b4..7d69360 100644
--- a/java/org/apache/catalina/users/GenericUser.java
+++ b/java/org/apache/catalina/users/GenericUser.java
@@ -29,6 +29,8 @@ import org.apache.catalina.UserDatabase;
  * <p>Concrete implementation of {@link org.apache.catalina.User} for a
  * {@link UserDatabase}.</p>
  *
+ * @param <UD> The specific type of UserDase with which this role is associated
+ *
  * @author Craig R. McClanahan
  */
 public class GenericUser<UD extends UserDatabase> extends AbstractUser {

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 03/16: Add available flag for UserDatabase

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 1e50c19b5f5164d4fbb38616af4c5548e44700f8
Author: remm <re...@apache.org>
AuthorDate: Wed Aug 25 22:36:17 2021 +0200

    Add available flag for UserDatabase
    
    Also avoid loading the whole database as MBeans, the behavior is fancy
    but only makes sense for a memory user database.
---
 java/org/apache/catalina/UserDatabase.java         | 22 ++++++++++++++++++++++
 .../mbeans/GlobalResourcesLifecycleListener.java   |  5 +++++
 .../apache/catalina/realm/UserDatabaseRealm.java   |  6 ++++++
 3 files changed, 33 insertions(+)

diff --git a/java/org/apache/catalina/UserDatabase.java b/java/org/apache/catalina/UserDatabase.java
index 9242170..713cc1d 100644
--- a/java/org/apache/catalina/UserDatabase.java
+++ b/java/org/apache/catalina/UserDatabase.java
@@ -198,4 +198,26 @@ public interface UserDatabase {
     public default void backgroundProcess() {
         // NO-OP by default
     }
+
+
+    /**
+     * Is the database available.
+     *
+     * @return true
+     */
+    public default boolean isAvailable() {
+        return true;
+    }
+
+
+    /**
+     * Is the database data loaded on demand. This is used to avoid eager
+     * loading of the full database data, for example for JMX registration of
+     * all objects.
+     *
+     * @return false
+     */
+    public default boolean isSparse() {
+        return false;
+    }
 }
diff --git a/java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java b/java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java
index a86b486..bca1ed7 100644
--- a/java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java
+++ b/java/org/apache/catalina/mbeans/GlobalResourcesLifecycleListener.java
@@ -168,6 +168,11 @@ public class GlobalResourcesLifecycleListener implements LifecycleListener {
             throw new IllegalArgumentException(sm.getString("globalResources.createError.userDatabase", name), e);
         }
 
+        if (database.isSparse()) {
+            // Avoid loading all the database as mbeans
+            return;
+        }
+
         // Create the MBeans for each defined Role
         Iterator<Role> roles = database.getRoles();
         while (roles.hasNext()) {
diff --git a/java/org/apache/catalina/realm/UserDatabaseRealm.java b/java/org/apache/catalina/realm/UserDatabaseRealm.java
index e76adbd..d98115f 100644
--- a/java/org/apache/catalina/realm/UserDatabaseRealm.java
+++ b/java/org/apache/catalina/realm/UserDatabaseRealm.java
@@ -231,6 +231,12 @@ public class UserDatabaseRealm extends RealmBase {
     }
 
 
+    @Override
+    public boolean isAvailable() {
+        return (database == null) ? false : database.isAvailable();
+    }
+
+
     public static final class UserDatabasePrincipal extends GenericPrincipal {
         private static final long serialVersionUID = 1L;
         private final transient User user;

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 15/16: Checkstyle

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit e3cf4ac94ec30f3a5415efb9234f42bf061ad39e
Author: remm <re...@apache.org>
AuthorDate: Wed Sep 1 13:45:18 2021 +0200

    Checkstyle
---
 java/org/apache/catalina/realm/UserDatabaseRealm.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/java/org/apache/catalina/realm/UserDatabaseRealm.java b/java/org/apache/catalina/realm/UserDatabaseRealm.java
index 8269a60..822c807 100644
--- a/java/org/apache/catalina/realm/UserDatabaseRealm.java
+++ b/java/org/apache/catalina/realm/UserDatabaseRealm.java
@@ -202,7 +202,7 @@ public class UserDatabaseRealm extends RealmBase {
             return null;
         } else {
             if (useStaticPrincipal) {
-                return new GenericPrincipal(username, null, Arrays.asList(getRoles(user))); 
+                return new GenericPrincipal(username, null, Arrays.asList(getRoles(user)));
             } else {
                 return new UserDatabasePrincipal(user, database);
             }

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 06/16: Add lock use similar to the memory user database

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 9eff810ba36aefb79b92a5d916526bc2e75499a6
Author: remm <re...@apache.org>
AuthorDate: Thu Aug 26 16:40:44 2021 +0200

    Add lock use similar to the memory user database
    
    Remove duplicated code, I verified the memory MBean works fine.
---
 .../catalina/mbeans/MemoryUserDatabaseMBean.java   | 277 +----------
 .../catalina/mbeans/SparseUserDatabaseMBean.java   |  59 +--
 .../catalina/users/DataSourceUserDatabase.java     | 553 ++++++++++++---------
 3 files changed, 353 insertions(+), 536 deletions(-)

diff --git a/java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java b/java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java
index b8fc248..7105cf8 100644
--- a/java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java
+++ b/java/org/apache/catalina/mbeans/MemoryUserDatabaseMBean.java
@@ -16,21 +16,7 @@
  */
 package org.apache.catalina.mbeans;
 
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-import javax.management.MalformedObjectNameException;
-import javax.management.ObjectName;
-
-import org.apache.catalina.Group;
-import org.apache.catalina.Role;
-import org.apache.catalina.User;
-import org.apache.catalina.UserDatabase;
-import org.apache.tomcat.util.modeler.BaseModelMBean;
 import org.apache.tomcat.util.modeler.ManagedBean;
-import org.apache.tomcat.util.modeler.Registry;
-import org.apache.tomcat.util.res.StringManager;
 
 /**
  * <p>A <strong>ModelMBean</strong> implementation for the
@@ -38,272 +24,11 @@ import org.apache.tomcat.util.res.StringManager;
  *
  * @author Craig R. McClanahan
  */
-public class MemoryUserDatabaseMBean extends BaseModelMBean {
-
-    private static final StringManager sm = StringManager.getManager(MemoryUserDatabaseMBean.class);
-
-    // ----------------------------------------------------- Instance Variables
-
-    /**
-     * The configuration information registry for our managed beans.
-     */
-    protected final Registry registry = MBeanUtils.createRegistry();
-
+public class MemoryUserDatabaseMBean extends SparseUserDatabaseMBean {
 
     /**
      * The <code>ManagedBean</code> information describing this MBean.
      */
     protected final ManagedBean managed = registry.findManagedBean("MemoryUserDatabase");
 
-
-    /**
-     * The <code>ManagedBean</code> information describing Group MBeans.
-     */
-    protected final ManagedBean managedGroup = registry.findManagedBean("Group");
-
-
-    /**
-     * The <code>ManagedBean</code> information describing Group MBeans.
-     */
-    protected final ManagedBean managedRole = registry.findManagedBean("Role");
-
-
-    /**
-     * The <code>ManagedBean</code> information describing User MBeans.
-     */
-    protected final ManagedBean managedUser = registry.findManagedBean("User");
-
-
-    // ------------------------------------------------------------- Attributes
-
-    /**
-     * @return the MBean Names of all groups defined in this database.
-     */
-    public String[] getGroups() {
-        UserDatabase database = (UserDatabase) this.resource;
-        List<String> results = new ArrayList<>();
-        Iterator<Group> groups = database.getGroups();
-        while (groups.hasNext()) {
-            Group group = groups.next();
-            results.add(findGroup(group.getGroupname()));
-        }
-        return results.toArray(new String[0]);
-    }
-
-
-    /**
-     * @return the MBean Names of all roles defined in this database.
-     */
-    public String[] getRoles() {
-        UserDatabase database = (UserDatabase) this.resource;
-        List<String> results = new ArrayList<>();
-        Iterator<Role> roles = database.getRoles();
-        while (roles.hasNext()) {
-            Role role = roles.next();
-            results.add(findRole(role.getRolename()));
-        }
-        return results.toArray(new String[0]);
-    }
-
-
-    /**
-     * @return the MBean Names of all users defined in this database.
-     */
-    public String[] getUsers() {
-        UserDatabase database = (UserDatabase) this.resource;
-        List<String> results = new ArrayList<>();
-        Iterator<User> users = database.getUsers();
-        while (users.hasNext()) {
-            User user = users.next();
-            results.add(findUser(user.getUsername()));
-        }
-        return results.toArray(new String[0]);
-    }
-
-
-    // ------------------------------------------------------------- Operations
-
-    /**
-     * Create a new Group and return the corresponding MBean Name.
-     *
-     * @param groupname Group name of the new group
-     * @param description Description of the new group
-     * @return the new group object name
-     */
-    public String createGroup(String groupname, String description) {
-        UserDatabase database = (UserDatabase) this.resource;
-        Group group = database.createGroup(groupname, description);
-        try {
-            MBeanUtils.createMBean(group);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.group", groupname), e);
-        }
-        return findGroup(groupname);
-    }
-
-
-    /**
-     * Create a new Role and return the corresponding MBean Name.
-     *
-     * @param rolename Group name of the new group
-     * @param description Description of the new group
-     * @return the new role object name
-     */
-    public String createRole(String rolename, String description) {
-        UserDatabase database = (UserDatabase) this.resource;
-        Role role = database.createRole(rolename, description);
-        try {
-            MBeanUtils.createMBean(role);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.role", rolename), e);
-        }
-        return findRole(rolename);
-    }
-
-
-    /**
-     * Create a new User and return the corresponding MBean Name.
-     *
-     * @param username User name of the new user
-     * @param password Password for the new user
-     * @param fullName Full name for the new user
-     * @return the new user object name
-     */
-    public String createUser(String username, String password, String fullName) {
-        UserDatabase database = (UserDatabase) this.resource;
-        User user = database.createUser(username, password, fullName);
-        try {
-            MBeanUtils.createMBean(user);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.user", username), e);
-        }
-        return findUser(username);
-    }
-
-
-    /**
-     * Return the MBean Name for the specified group name (if any);
-     * otherwise return <code>null</code>.
-     *
-     * @param groupname Group name to look up
-     * @return the group object name
-     */
-    public String findGroup(String groupname) {
-        UserDatabase database = (UserDatabase) this.resource;
-        Group group = database.findGroup(groupname);
-        if (group == null) {
-            return null;
-        }
-        try {
-            ObjectName oname = MBeanUtils.createObjectName(managedGroup.getDomain(), group);
-            return oname.toString();
-        } catch (MalformedObjectNameException e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.createError.group", groupname), e);
-        }
-    }
-
-
-    /**
-     * Return the MBean Name for the specified role name (if any);
-     * otherwise return <code>null</code>.
-     *
-     * @param rolename Role name to look up
-     * @return the role object name
-     */
-    public String findRole(String rolename) {
-        UserDatabase database = (UserDatabase) this.resource;
-        Role role = database.findRole(rolename);
-        if (role == null) {
-            return null;
-        }
-        try {
-            ObjectName oname = MBeanUtils.createObjectName(managedRole.getDomain(), role);
-            return oname.toString();
-        } catch (MalformedObjectNameException e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.createError.role", rolename), e);
-        }
-
-    }
-
-
-    /**
-     * Return the MBean Name for the specified user name (if any);
-     * otherwise return <code>null</code>.
-     *
-     * @param username User name to look up
-     * @return the user object name
-     */
-    public String findUser(String username) {
-        UserDatabase database = (UserDatabase) this.resource;
-        User user = database.findUser(username);
-        if (user == null) {
-            return null;
-        }
-        try {
-            ObjectName oname = MBeanUtils.createObjectName(managedUser.getDomain(), user);
-            return oname.toString();
-        } catch (MalformedObjectNameException e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.createError.user", username), e);
-        }
-    }
-
-
-    /**
-     * Remove an existing group and destroy the corresponding MBean.
-     *
-     * @param groupname Group name to remove
-     */
-    public void removeGroup(String groupname) {
-        UserDatabase database = (UserDatabase) this.resource;
-        Group group = database.findGroup(groupname);
-        if (group == null) {
-            return;
-        }
-        try {
-            MBeanUtils.destroyMBean(group);
-            database.removeGroup(group);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.destroyError.group", groupname), e);
-        }
-    }
-
-
-    /**
-     * Remove an existing role and destroy the corresponding MBean.
-     *
-     * @param rolename Role name to remove
-     */
-    public void removeRole(String rolename) {
-        UserDatabase database = (UserDatabase) this.resource;
-        Role role = database.findRole(rolename);
-        if (role == null) {
-            return;
-        }
-        try {
-            MBeanUtils.destroyMBean(role);
-            database.removeRole(role);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.destroyError.role", rolename), e);
-        }
-    }
-
-
-    /**
-     * Remove an existing user and destroy the corresponding MBean.
-     *
-     * @param username User name to remove
-     */
-    public void removeUser(String username) {
-        UserDatabase database = (UserDatabase) this.resource;
-        User user = database.findUser(username);
-        if (user == null) {
-            return;
-        }
-        try {
-            MBeanUtils.destroyMBean(user);
-            database.removeUser(user);
-        } catch (Exception e) {
-            throw new IllegalArgumentException(sm.getString("userMBean.destroyError.user", username), e);
-        }
-    }
 }
diff --git a/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java b/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java
index 921424d..09710dd 100644
--- a/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java
+++ b/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java
@@ -207,7 +207,7 @@ public class SparseUserDatabaseMBean extends BaseModelMBean {
         }
         try {
             ObjectName oname = MBeanUtils.createObjectName(managedGroup.getDomain(), group);
-            if (!mserver.isRegistered(oname)) {
+            if (database.isSparse() && !mserver.isRegistered(oname)) {
                 MBeanUtils.createMBean(group);
             }
             return oname.toString();
@@ -232,7 +232,7 @@ public class SparseUserDatabaseMBean extends BaseModelMBean {
         }
         try {
             ObjectName oname = MBeanUtils.createObjectName(managedRole.getDomain(), role);
-            if (!mserver.isRegistered(oname)) {
+            if (database.isSparse() && !mserver.isRegistered(oname)) {
                 MBeanUtils.createMBean(role);
             }
             return oname.toString();
@@ -258,7 +258,7 @@ public class SparseUserDatabaseMBean extends BaseModelMBean {
         }
         try {
             ObjectName oname = MBeanUtils.createObjectName(managedUser.getDomain(), user);
-            if (!mserver.isRegistered(oname)) {
+            if (database.isSparse() && !mserver.isRegistered(oname)) {
                 MBeanUtils.createMBean(user);
             }
             return oname.toString();
@@ -334,33 +334,34 @@ public class SparseUserDatabaseMBean extends BaseModelMBean {
     public void save() {
         try {
             UserDatabase database = (UserDatabase) this.resource;
-            ObjectName query = null;
-            Set<ObjectName> results = null;
-
-            // Groups
-            query = new ObjectName(
-                    "Users:type=Group,database=" + database.getId() + ",*");
-            results = mserver.queryNames(query, null);
-            for (ObjectName result : results) {
-                mserver.unregisterMBean(result);
+            if (database.isSparse()) {
+                ObjectName query = null;
+                Set<ObjectName> results = null;
+
+                // Groups
+                query = new ObjectName(
+                        "Users:type=Group,database=" + database.getId() + ",*");
+                results = mserver.queryNames(query, null);
+                for (ObjectName result : results) {
+                    mserver.unregisterMBean(result);
+                }
+
+                // Roles
+                query = new ObjectName(
+                        "Users:type=Role,database=" + database.getId() + ",*");
+                results = mserver.queryNames(query, null);
+                for (ObjectName result : results) {
+                    mserver.unregisterMBean(result);
+                }
+
+                // Users
+                query = new ObjectName(
+                        "Users:type=User,database=" + database.getId() + ",*");
+                results = mserver.queryNames(query, null);
+                for (ObjectName result : results) {
+                    mserver.unregisterMBean(result);
+                }
             }
-
-            // Roles
-            query = new ObjectName(
-                    "Users:type=Role,database=" + database.getId() + ",*");
-            results = mserver.queryNames(query, null);
-            for (ObjectName result : results) {
-                mserver.unregisterMBean(result);
-            }
-
-            // Users
-            query = new ObjectName(
-                    "Users:type=User,database=" + database.getId() + ",*");
-            results = mserver.queryNames(query, null);
-            for (ObjectName result : results) {
-                mserver.unregisterMBean(result);
-            }
-
             database.save();
         } catch (Exception e) {
             throw new IllegalArgumentException(sm.getString("userMBean.saveError"), e);
diff --git a/java/org/apache/catalina/users/DataSourceUserDatabase.java b/java/org/apache/catalina/users/DataSourceUserDatabase.java
index 0420e51..f90d1b0 100644
--- a/java/org/apache/catalina/users/DataSourceUserDatabase.java
+++ b/java/org/apache/catalina/users/DataSourceUserDatabase.java
@@ -24,6 +24,8 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import javax.naming.Context;
 import javax.sql.DataSource;
@@ -219,6 +221,11 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
     protected boolean readonly = true;
 
 
+    private final ReentrantReadWriteLock dbLock = new ReentrantReadWriteLock();
+    private final Lock readLock = dbLock.readLock();
+    private final Lock writeLock = dbLock.writeLock();
+
+
     // ------------------------------------------------------------- Properties
 
 
@@ -438,98 +445,110 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
 
     @Override
     public Iterator<Group> getGroups() {
-        HashMap<String, Group> groups = new HashMap<>();
-        groups.putAll(createdGroups);
-        groups.putAll(modifiedGroups);
-
-        Connection dbConnection = openConnection();
-        if (dbConnection != null && preparedAllGroups != null) {
-            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllGroups)) {
-                try (ResultSet rs = stmt.executeQuery()) {
-                    while (rs.next()) {
-                        String groupName = rs.getString(1);
-                        if (groupName != null) {
-                            if (!groups.containsKey(groupName) && !removedGroups.containsKey(groupName)) {
-                                Group group = findGroupInternal(dbConnection, groupName);
-                                if (group != null) {
-                                    groups.put(groupName, group);
+        readLock.lock();
+        try {
+            HashMap<String, Group> groups = new HashMap<>();
+            groups.putAll(createdGroups);
+            groups.putAll(modifiedGroups);
+
+            Connection dbConnection = openConnection();
+            if (dbConnection != null && preparedAllGroups != null) {
+                try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllGroups)) {
+                    try (ResultSet rs = stmt.executeQuery()) {
+                        while (rs.next()) {
+                            String groupName = rs.getString(1);
+                            if (groupName != null) {
+                                if (!groups.containsKey(groupName) && !removedGroups.containsKey(groupName)) {
+                                    Group group = findGroupInternal(dbConnection, groupName);
+                                    if (group != null) {
+                                        groups.put(groupName, group);
+                                    }
                                 }
                             }
                         }
                     }
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                } finally {
+                    close(dbConnection);
                 }
-            } catch (SQLException e) {
-                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
-            } finally {
-                close(dbConnection);
             }
+            return groups.values().iterator();
+        } finally {
+            readLock.unlock();
         }
-
-        return groups.values().iterator();
     }
 
     @Override
     public Iterator<Role> getRoles() {
-        HashMap<String, Role> roles = new HashMap<>();
-        roles.putAll(createdRoles);
-        roles.putAll(modifiedRoles);
-
-        Connection dbConnection = openConnection();
-        if (dbConnection != null && preparedAllRoles != null) {
-            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllRoles)) {
-                try (ResultSet rs = stmt.executeQuery()) {
-                    while (rs.next()) {
-                        String roleName = rs.getString(1);
-                        if (roleName != null) {
-                            if (!roles.containsKey(roleName) && !removedRoles.containsKey(roleName)) {
-                                Role role = findRoleInternal(dbConnection, roleName);
-                                if (role != null) {
-                                    roles.put(roleName, role);
+        readLock.lock();
+        try {
+            HashMap<String, Role> roles = new HashMap<>();
+            roles.putAll(createdRoles);
+            roles.putAll(modifiedRoles);
+
+            Connection dbConnection = openConnection();
+            if (dbConnection != null && preparedAllRoles != null) {
+                try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllRoles)) {
+                    try (ResultSet rs = stmt.executeQuery()) {
+                        while (rs.next()) {
+                            String roleName = rs.getString(1);
+                            if (roleName != null) {
+                                if (!roles.containsKey(roleName) && !removedRoles.containsKey(roleName)) {
+                                    Role role = findRoleInternal(dbConnection, roleName);
+                                    if (role != null) {
+                                        roles.put(roleName, role);
+                                    }
                                 }
                             }
                         }
                     }
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                } finally {
+                    close(dbConnection);
                 }
-            } catch (SQLException e) {
-                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
-            } finally {
-                close(dbConnection);
             }
+            return roles.values().iterator();
+        } finally {
+            readLock.unlock();
         }
-
-        return roles.values().iterator();
     }
 
     @Override
     public Iterator<User> getUsers() {
-        HashMap<String, User> users = new HashMap<>();
-        users.putAll(createdUsers);
-        users.putAll(modifiedUsers);
-
-        Connection dbConnection = openConnection();
-        if (dbConnection != null) {
-            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllUsers)) {
-                try (ResultSet rs = stmt.executeQuery()) {
-                    while (rs.next()) {
-                        String userName = rs.getString(1);
-                        if (userName != null) {
-                            if (!users.containsKey(userName) && !removedUsers.containsKey(userName)) {
-                                User user = findUserInternal(dbConnection, userName);
-                                if (user != null) {
-                                    users.put(userName, user);
+        readLock.lock();
+        try {
+            HashMap<String, User> users = new HashMap<>();
+            users.putAll(createdUsers);
+            users.putAll(modifiedUsers);
+
+            Connection dbConnection = openConnection();
+            if (dbConnection != null) {
+                try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllUsers)) {
+                    try (ResultSet rs = stmt.executeQuery()) {
+                        while (rs.next()) {
+                            String userName = rs.getString(1);
+                            if (userName != null) {
+                                if (!users.containsKey(userName) && !removedUsers.containsKey(userName)) {
+                                    User user = findUserInternal(dbConnection, userName);
+                                    if (user != null) {
+                                        users.put(userName, user);
+                                    }
                                 }
                             }
                         }
                     }
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                } finally {
+                    close(dbConnection);
                 }
-            } catch (SQLException e) {
-                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
-            } finally {
-                close(dbConnection);
             }
+            return users.values().iterator();
+        } finally {
+            readLock.unlock();
         }
-
-        return users.values().iterator();
     }
 
     @Override
@@ -538,55 +557,75 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
 
     @Override
     public Group createGroup(String groupname, String description) {
-        Group group = new GenericGroup<DataSourceUserDatabase>(this, groupname, description, null);
-        createdGroups.put(groupname, group);
-        modifiedGroups.remove(groupname);
-        removedGroups.remove(groupname);
-        return group;
+        readLock.lock();
+        try {
+            Group group = new GenericGroup<DataSourceUserDatabase>(this, groupname, description, null);
+            createdGroups.put(groupname, group);
+            modifiedGroups.remove(groupname);
+            removedGroups.remove(groupname);
+            return group;
+        } finally {
+            readLock.unlock();
+        }
     }
 
     @Override
     public Role createRole(String rolename, String description) {
-        Role role = new GenericRole<DataSourceUserDatabase>(this, rolename, description);
-        createdRoles.put(rolename, role);
-        modifiedRoles.remove(rolename);
-        removedRoles.remove(rolename);
-        return role;
+        readLock.lock();
+        try {
+            Role role = new GenericRole<DataSourceUserDatabase>(this, rolename, description);
+            createdRoles.put(rolename, role);
+            modifiedRoles.remove(rolename);
+            removedRoles.remove(rolename);
+            return role;
+        } finally {
+            readLock.unlock();
+        }
     }
 
     @Override
     public User createUser(String username, String password, String fullName) {
-        User user = new GenericUser<DataSourceUserDatabase>(this, username, password, fullName, null, null);
-        createdUsers.put(username, user);
-        modifiedUsers.remove(username);
-        removedUsers.remove(username);
-        return user;
+        readLock.lock();
+        try {
+            User user = new GenericUser<DataSourceUserDatabase>(this, username, password, fullName, null, null);
+            createdUsers.put(username, user);
+            modifiedUsers.remove(username);
+            removedUsers.remove(username);
+            return user;
+        } finally {
+            readLock.unlock();
+        }
     }
 
     @Override
     public Group findGroup(String groupname) {
-        // Check local changes first
-        Group group = createdGroups.get(groupname);
-        if (group != null) {
-            return group;
-        }
-        group = modifiedGroups.get(groupname);
-        if (group != null) {
-            return group;
-        }
-        group = removedGroups.get(groupname);
-        if (group != null) {
-            return null;
-        }
-
-        Connection dbConnection = openConnection();
-        if (dbConnection == null) {
-            return null;
-        }
+        readLock.lock();
         try {
-            return findGroupInternal(dbConnection, groupname);
+            // Check local changes first
+            Group group = createdGroups.get(groupname);
+            if (group != null) {
+                return group;
+            }
+            group = modifiedGroups.get(groupname);
+            if (group != null) {
+                return group;
+            }
+            group = removedGroups.get(groupname);
+            if (group != null) {
+                return null;
+            }
+
+            Connection dbConnection = openConnection();
+            if (dbConnection == null) {
+                return null;
+            }
+            try {
+                return findGroupInternal(dbConnection, groupname);
+            } finally {
+                close(dbConnection);
+            }
         } finally {
-            close(dbConnection);
+            readLock.unlock();
         }
     }
 
@@ -630,28 +669,33 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
 
     @Override
     public Role findRole(String rolename) {
-        // Check local changes first
-        Role role = createdRoles.get(rolename);
-        if (role != null) {
-            return role;
-        }
-        role = modifiedRoles.get(rolename);
-        if (role != null) {
-            return role;
-        }
-        role = removedRoles.get(rolename);
-        if (role != null) {
-            return null;
-        }
-
-        Connection dbConnection = openConnection();
-        if (dbConnection == null) {
-            return null;
-        }
+        readLock.lock();
         try {
-            return findRoleInternal(dbConnection, rolename);
+            // Check local changes first
+            Role role = createdRoles.get(rolename);
+            if (role != null) {
+                return role;
+            }
+            role = modifiedRoles.get(rolename);
+            if (role != null) {
+                return role;
+            }
+            role = removedRoles.get(rolename);
+            if (role != null) {
+                return null;
+            }
+
+            Connection dbConnection = openConnection();
+            if (dbConnection == null) {
+                return null;
+            }
+            try {
+                return findRoleInternal(dbConnection, rolename);
+            } finally {
+                close(dbConnection);
+            }
         } finally {
-            close(dbConnection);
+            readLock.unlock();
         }
     }
 
@@ -675,28 +719,33 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
 
     @Override
     public User findUser(String username) {
-        // Check local changes first
-        User user = createdUsers.get(username);
-        if (user != null) {
-            return user;
-        }
-        user = modifiedUsers.get(username);
-        if (user != null) {
-            return user;
-        }
-        user = removedUsers.get(username);
-        if (user != null) {
-            return null;
-        }
-
-        Connection dbConnection = openConnection();
-        if (dbConnection == null) {
-            return null;
-        }
+        readLock.lock();
         try {
-            return findUserInternal(dbConnection, username);
+            // Check local changes first
+            User user = createdUsers.get(username);
+            if (user != null) {
+                return user;
+            }
+            user = modifiedUsers.get(username);
+            if (user != null) {
+                return user;
+            }
+            user = removedUsers.get(username);
+            if (user != null) {
+                return null;
+            }
+
+            Connection dbConnection = openConnection();
+            if (dbConnection == null) {
+                return null;
+            }
+            try {
+                return findUserInternal(dbConnection, username);
+            } finally {
+                close(dbConnection);
+            }
         } finally {
-            close(dbConnection);
+            readLock.unlock();
         }
     }
 
@@ -769,145 +818,182 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
 
     @Override
     public void modifiedGroup(Group group) {
-        String name = group.getName();
-        if (!createdGroups.containsKey(name) && !removedGroups.containsKey(name)) {
-            modifiedGroups.put(name, group);
+        readLock.lock();
+        try {
+            String name = group.getName();
+            if (!createdGroups.containsKey(name) && !removedGroups.containsKey(name)) {
+                modifiedGroups.put(name, group);
+            }
+        } finally {
+            readLock.unlock();
         }
     }
 
     @Override
     public void modifiedRole(Role role) {
-        String name = role.getName();
-        if (!createdRoles.containsKey(name) && !removedRoles.containsKey(name)) {
-            modifiedRoles.put(name, role);
+        readLock.lock();
+        try {
+            String name = role.getName();
+            if (!createdRoles.containsKey(name) && !removedRoles.containsKey(name)) {
+                modifiedRoles.put(name, role);
+            }
+        } finally {
+            readLock.unlock();
         }
     }
 
     @Override
     public void modifiedUser(User user) {
-        String name = user.getName();
-        if (!createdUsers.containsKey(name) && !removedUsers.containsKey(name)) {
-            modifiedUsers.put(name, user);
+        readLock.lock();
+        try {
+            String name = user.getName();
+            if (!createdUsers.containsKey(name) && !removedUsers.containsKey(name)) {
+                modifiedUsers.put(name, user);
+            }
+        } finally {
+            readLock.unlock();
         }
     }
 
     @Override
     public void open() throws Exception {
 
-        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();
+        writeLock.lock();
+        try {
 
-        if (userGroupTable != null && userGroupTable.length() > 0) {
-            temp = new StringBuilder("SELECT ");
-            temp.append(groupNameCol);
+            StringBuilder temp = new StringBuilder("SELECT ");
+            temp.append(roleNameCol);
             temp.append(" FROM ");
-            temp.append(userGroupTable);
+            temp.append(userRoleTable);
             temp.append(" WHERE ");
             temp.append(userNameCol);
             temp.append(" = ?");
-            preparedGroups = temp.toString();
-        }
+            preparedRoles = temp.toString();
+
+            if (userGroupTable != null && userGroupTable.length() > 0) {
+                temp = new StringBuilder("SELECT ");
+                temp.append(groupNameCol);
+                temp.append(" FROM ");
+                temp.append(userGroupTable);
+                temp.append(" WHERE ");
+                temp.append(userNameCol);
+                temp.append(" = ?");
+                preparedGroups = temp.toString();
+            }
 
-        if (groupRoleTable != null && groupRoleTable.length() > 0) {
-            temp = new StringBuilder("SELECT ");
-            temp.append(groupNameCol);
-            temp.append(" FROM ");
-            temp.append(groupRoleTable);
-            temp.append(" WHERE ");
-            temp.append(groupNameCol);
-            temp.append(" = ?");
-            preparedGroupsR = temp.toString();
-        }
+            if (groupRoleTable != null && groupRoleTable.length() > 0) {
+                temp = new StringBuilder("SELECT ");
+                temp.append(groupNameCol);
+                temp.append(" FROM ");
+                temp.append(groupRoleTable);
+                temp.append(" WHERE ");
+                temp.append(groupNameCol);
+                temp.append(" = ?");
+                preparedGroupsR = temp.toString();
+            }
 
-        temp = new StringBuilder("SELECT ");
-        temp.append(userCredCol);
-        if (userFullNameCol != null) {
-            temp.append(",").append(userFullNameCol);
-        }
-        temp.append(" FROM ");
-        temp.append(userTable);
-        temp.append(" WHERE ");
-        temp.append(userNameCol);
-        temp.append(" = ?");
-        preparedUser = temp.toString();
-
-        temp = new StringBuilder("SELECT ");
-        temp.append(userNameCol);
-        temp.append(" FROM ");
-        temp.append(userTable);
-        preparedAllUsers = temp.toString();
-
-        if (groupTable != null && groupTable.length() > 0) {
             temp = new StringBuilder("SELECT ");
-            temp.append(groupNameCol);
-            if (roleAndGroupDescriptionCol != null) {
-                temp.append(",").append(roleAndGroupDescriptionCol);
+            temp.append(userCredCol);
+            if (userFullNameCol != null) {
+                temp.append(",").append(userFullNameCol);
             }
             temp.append(" FROM ");
-            temp.append(groupTable);
+            temp.append(userTable);
             temp.append(" WHERE ");
-            temp.append(groupNameCol);
+            temp.append(userNameCol);
             temp.append(" = ?");
-            preparedGroup = temp.toString();
+            preparedUser = temp.toString();
 
             temp = new StringBuilder("SELECT ");
-            temp.append(groupNameCol);
+            temp.append(userNameCol);
             temp.append(" FROM ");
-            temp.append(groupTable);
-            preparedAllGroups = temp.toString();
-        }
+            temp.append(userTable);
+            preparedAllUsers = temp.toString();
 
-        if (roleTable != null && roleTable.length() > 0) {
-            // Create the role PreparedStatement string
-            temp = new StringBuilder("SELECT ");
-            temp.append(roleNameCol);
-            if (roleAndGroupDescriptionCol != null) {
-                temp.append(",").append(roleAndGroupDescriptionCol);
+            if (groupTable != null && groupTable.length() > 0) {
+                temp = new StringBuilder("SELECT ");
+                temp.append(groupNameCol);
+                if (roleAndGroupDescriptionCol != null) {
+                    temp.append(",").append(roleAndGroupDescriptionCol);
+                }
+                temp.append(" FROM ");
+                temp.append(groupTable);
+                temp.append(" WHERE ");
+                temp.append(groupNameCol);
+                temp.append(" = ?");
+                preparedGroup = temp.toString();
+
+                temp = new StringBuilder("SELECT ");
+                temp.append(groupNameCol);
+                temp.append(" FROM ");
+                temp.append(groupTable);
+                preparedAllGroups = temp.toString();
             }
-            temp.append(" FROM ");
-            temp.append(roleTable);
-            temp.append(" WHERE ");
-            temp.append(roleNameCol);
-            temp.append(" = ?");
-            preparedRole = temp.toString();
 
-            temp = new StringBuilder("SELECT ");
-            temp.append(roleNameCol);
-            temp.append(" FROM ");
-            temp.append(roleTable);
-            preparedAllRoles = temp.toString();
+            if (roleTable != null && roleTable.length() > 0) {
+                // Create the role PreparedStatement string
+                temp = new StringBuilder("SELECT ");
+                temp.append(roleNameCol);
+                if (roleAndGroupDescriptionCol != null) {
+                    temp.append(",").append(roleAndGroupDescriptionCol);
+                }
+                temp.append(" FROM ");
+                temp.append(roleTable);
+                temp.append(" WHERE ");
+                temp.append(roleNameCol);
+                temp.append(" = ?");
+                preparedRole = temp.toString();
+
+                temp = new StringBuilder("SELECT ");
+                temp.append(roleNameCol);
+                temp.append(" FROM ");
+                temp.append(roleTable);
+                preparedAllRoles = temp.toString();
+            }
+
+        } finally {
+            writeLock.unlock();
         }
 
     }
 
     @Override
     public void removeGroup(Group group) {
-        String name = group.getName();
-        createdGroups.remove(name);
-        modifiedGroups.remove(name);
-        removedGroups.put(name, group);
+        readLock.lock();
+        try {
+            String name = group.getName();
+            createdGroups.remove(name);
+            modifiedGroups.remove(name);
+            removedGroups.put(name, group);
+        } finally {
+            readLock.unlock();
+        }
     }
 
     @Override
     public void removeRole(Role role) {
-        String name = role.getName();
-        createdRoles.remove(name);
-        modifiedRoles.remove(name);
-        removedRoles.put(name, role);
+        readLock.lock();
+        try {
+            String name = role.getName();
+            createdRoles.remove(name);
+            modifiedRoles.remove(name);
+            removedRoles.put(name, role);
+        } finally {
+            readLock.unlock();
+        }
     }
 
     @Override
     public void removeUser(User user) {
-        String name = user.getName();
-        createdUsers.remove(name);
-        modifiedUsers.remove(name);
-        removedUsers.put(name, user);
+        readLock.lock();
+        try {
+            String name = user.getName();
+            createdUsers.remove(name);
+            modifiedUsers.remove(name);
+            removedUsers.put(name, user);
+        } finally {
+            readLock.unlock();
+        }
     }
 
     @Override
@@ -921,10 +1007,15 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
             return;
         }
 
+        writeLock.lock();
         try {
-            saveInternal(dbConnection);
+            try {
+                saveInternal(dbConnection);
+            } finally {
+                close(dbConnection);
+            }
         } finally {
-            close(dbConnection);
+            writeLock.unlock();
         }
     }
 

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 13/16: Add first pass at tests for the DataSource UserDatabase

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 7375b36f8a5e67a14a13214ffa6bdae23cc3248c
Author: remm <re...@apache.org>
AuthorDate: Tue Aug 31 21:37:47 2021 +0200

    Add first pass at tests for the DataSource UserDatabase
    
    Use a shortcut to allow independent testing without a real DataSource.
    Test both the DataSourceRealm schema as well as the full schema with
    groups.
    Only glitch is that Derby leaves a derby.log, I haven't found how to
    configure it away.
---
 .../users/DataSourceUserDatabaseTests.java         | 228 +++++++++++++++++++++
 1 file changed, 228 insertions(+)

diff --git a/test/org/apache/catalina/users/DataSourceUserDatabaseTests.java b/test/org/apache/catalina/users/DataSourceUserDatabaseTests.java
new file mode 100644
index 0000000..5529917
--- /dev/null
+++ b/test/org/apache/catalina/users/DataSourceUserDatabaseTests.java
@@ -0,0 +1,228 @@
+/*
+ *  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.users;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.Iterator;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Group;
+import org.apache.catalina.Role;
+import org.apache.catalina.User;
+import org.apache.catalina.startup.LoggingBaseTest;
+
+public class DataSourceUserDatabaseTests extends LoggingBaseTest {
+
+    public static final String SIMPLE_SCHEMA =
+            "create table users (\n"
+            + "  user_name         varchar(15) not null primary key,\n"
+            + "  user_pass         varchar(15) not null\n"
+            + ");\n"
+            + "create table user_roles (\n"
+            + "  user_name         varchar(15) not null,\n"
+            + "  role_name         varchar(15) not null,\n"
+            + "  primary key (user_name, role_name)\n"
+            + ");";
+
+    public static final String FULL_SCHEMA =
+            "create table users (\n"
+            + "  user_name         varchar(15) not null primary key,\n"
+            + "  user_pass         varchar(15) not null,\n"
+            + "  user_fullname     varchar(128)\n"
+            + "  -- Add more attributes as needed\n"
+            + ");\n"
+            + "create table roles (\n"
+            + "  role_name         varchar(15) not null primary key,\n"
+            + "  role_description  varchar(128)\n"
+            + ");\n"
+            + "create table groups (\n"
+            + "  group_name        varchar(15) not null primary key,\n"
+            + "  group_description varchar(128)\n"
+            + ");\n"
+            + "create table user_roles (\n"
+            + "  user_name         varchar(15) references users(user_name),\n"
+            + "  role_name         varchar(15) references roles(role_name),\n"
+            + "  primary key (user_name, role_name)\n"
+            + ");\n"
+            + "create table user_groups (\n"
+            + "  user_name         varchar(15) references users(user_name),\n"
+            + "  group_name        varchar(15) references groups(group_name),\n"
+            + "  primary key (user_name, group_name)\n"
+            + ");\n"
+            + "create table group_roles (\n"
+            + "  group_name        varchar(15) references groups(group_name),\n"
+            + "  role_name         varchar(15) references roles(role_name),\n"
+            + "  primary key (group_name, role_name)\n"
+            + ");";
+
+    protected class DerbyUserDatabase extends DataSourceUserDatabase {
+        protected final String name;
+        protected Connection connection = null;
+        public DerbyUserDatabase(String name) {
+            super(null, "tomcat");
+            this.name = "/" + name;
+        }
+        @Override
+        protected Connection openConnection() {
+            // Replace DataSource use and JNDI access with direct Derby
+            // connection
+            return connection;
+        }
+        @Override
+        protected void close(Connection dbConnection) {
+        }
+        @Override
+        public void close() throws Exception {
+            if (connection != null) {
+                connection.close();
+            }
+        }
+        @Override
+        public void open() throws Exception {
+            super.open();
+            Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
+            connection = DriverManager.getConnection("jdbc:derby:" + getTemporaryDirectory().getAbsolutePath()
+                    + name + ";create=true");
+        }
+        public Connection getConnection() {
+            return connection;
+        }
+    }
+
+    private DerbyUserDatabase db;
+
+    @Test
+    public void testBasicUserRoleDatabase()
+        throws Exception {
+        // Test functionality with the DataSourceRealm schema
+
+        db = new DerbyUserDatabase("simple");
+        db.setReadonly(false);
+        db.setUserTable("users");
+        db.setUserNameCol("user_name");
+        db.setUserCredCol("user_pass");
+        db.setUserRoleTable("user_roles");
+        db.setRoleNameCol("role_name");
+        db.open();
+        // First create the DB tables
+        Connection connection = db.getConnection();
+        for (String sql: SIMPLE_SCHEMA.split(";")) {
+            try (Statement statement = connection.createStatement()) {
+                statement.execute(sql);
+            }
+        }
+
+        Iterator<User> users = db.getUsers();
+        Assert.assertFalse("Some users found", users.hasNext());
+
+        User tomcatUser = db.createUser("tomcat", "password", "A new user");
+        Role adminRole = db.createRole("admin", "Admin role");
+        Role managerRole = db.createRole("manager", "Manager role");
+        Role userRole = db.createRole("user", "User role");
+        tomcatUser.addRole(adminRole);
+        tomcatUser.addRole(userRole);
+        db.save();
+
+        users = db.getUsers();
+        Assert.assertTrue("No users found", users.hasNext());
+        tomcatUser = users.next();
+        Assert.assertTrue("Wrong user", tomcatUser.getUsername().equals("tomcat"));
+        Assert.assertTrue("Wrong password", tomcatUser.getPassword().equals("password"));
+        // Cannot save the user full name
+        Assert.assertNull("Wrong user fullname", tomcatUser.getFullName());
+        adminRole = db.findRole("admin");
+        Assert.assertNotNull("No admin role", adminRole);
+        Assert.assertTrue("No role for user", tomcatUser.isInRole(adminRole));
+        // Manager role cannot be saved, but remains valid in memory
+        managerRole = db.findRole("manager");
+        Assert.assertFalse("Unexpected role for user", tomcatUser.isInRole(managerRole));
+
+        db.close();
+    }
+
+    @Test
+    public void testUserDatabase()
+        throws Exception {
+
+        db = new DerbyUserDatabase("full");
+        db.setReadonly(false);
+        db.setUserTable("users");
+        db.setUserNameCol("user_name");
+        db.setUserCredCol("user_pass");
+        db.setUserRoleTable("user_roles");
+        db.setUserGroupTable("user_groups");
+        db.setRoleTable("roles");
+        db.setRoleNameCol("role_name");
+        db.setGroupTable("groups");
+        db.setGroupNameCol("group_name");
+        db.setGroupRoleTable("group_roles");
+        // Not setting the description or full name since it allows checking persistence,
+        // as any modification is kept in memory until save()
+        db.open();
+        // First create the DB tables
+        Connection connection = db.getConnection();
+        for (String sql: FULL_SCHEMA.split(";")) {
+            try (Statement statement = connection.createStatement()) {
+                statement.execute(sql);
+            }
+        }
+
+        Iterator<User> users = db.getUsers();
+        Assert.assertFalse("Some users found", users.hasNext());
+
+        User tomcatUser = db.createUser("tomcat", "password", "A new user");
+        User randomUser = db.createUser("random", "password", "Another new user");
+        Role adminRole = db.createRole("admin", "Admin role");
+        Role managerRole = db.createRole("manager", "Manager role");
+        Role userRole = db.createRole("user", "User role");
+        Group userGroup = db.createGroup("users", "All users");
+        userGroup.addRole(userRole);
+        tomcatUser.addRole(adminRole);
+        tomcatUser.addGroup(userGroup);
+        randomUser.addGroup(userGroup);
+        db.save();
+
+        users = db.getUsers();
+        Assert.assertTrue("No users found", users.hasNext());
+        tomcatUser = users.next();
+        if (!tomcatUser.getUsername().equals("tomcat")) {
+            tomcatUser = users.next();
+        }
+        Assert.assertTrue("Wrong user", tomcatUser.getUsername().equals("tomcat"));
+        Assert.assertTrue("Wrong password", tomcatUser.getPassword().equals("password"));
+        // Cannot save the user full name
+        Assert.assertNull("Wrong user fullname", tomcatUser.getFullName());
+        adminRole = db.findRole("admin");
+        Assert.assertNotNull("No admin role", adminRole);
+        Assert.assertNull("Wrong admin role", adminRole.getDescription());
+        Assert.assertTrue("No role for user", tomcatUser.isInRole(adminRole));
+        managerRole = db.findRole("manager");
+        Assert.assertFalse("Unexpected role for user", tomcatUser.isInRole(managerRole));
+        userRole = db.findRole("user");
+        userGroup = db.findGroup("users");
+        Assert.assertNull("Wrong users group", userGroup.getDescription());
+        Assert.assertTrue("No role for group", userGroup.isInRole(userRole));
+        randomUser = db.findUser("random");
+        Assert.assertTrue("No group for user", randomUser.isInGroup(userGroup));
+
+        db.close();
+    }
+}

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 11/16: Add Derby for the testsuite

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 73b125b58d167db541281d219c2788be71fdee60
Author: remm <re...@apache.org>
AuthorDate: Tue Aug 31 21:32:10 2021 +0200

    Add Derby for the testsuite
    
    It can be used for JDBC testing. Also for DataSource with our pools.
---
 build.properties.default   | 24 ++++++++++++++++++++++++
 build.xml                  | 30 ++++++++++++++++++++++++++++++
 webapps/docs/changelog.xml |  7 +++++++
 3 files changed, 61 insertions(+)

diff --git a/build.properties.default b/build.properties.default
index f2208e7..80dd4fa 100644
--- a/build.properties.default
+++ b/build.properties.default
@@ -321,3 +321,27 @@ jsign.checksum.value=50982facf864df4f08a051b43f7d2e42|3d203fbffdc45e837f98b7c9c5
 jsign.home=${base.path}/jsign-${jsign.version}
 jsign.jar=${jsign.home}/jsign-${jsign.version}.jar
 jsign.loc=${base-maven.loc}/net/jsign/jsign/${jsign.version}/jsign-${jsign.version}.jar
+
+# ----- Derby, used by unit tests -----
+derby.version=10.15.2.0
+
+# checksums for Derby 10.15.2.0
+derby.checksum.enabled=true
+derby.checksum.algorithm=MD5|SHA-1
+derby.checksum.value=abff01351b19bc62a188bac08a8bb58b|b64da6681994f33ba5783ffae55cdb44885b9e70
+derby-shared.checksum.enabled=true
+derby-shared.checksum.algorithm=MD5|SHA-1
+derby-shared.checksum.value=2cb9ab8b9cfb06c2da5a1d3825d04344|ff2dfb3e2a92d593cf111baad242d156947abbc1
+derby-tools.checksum.enabled=true
+derby-tools.checksum.algorithm=MD5|SHA-1
+derby-tools.checksum.value=d41578eeb336b0e479be8f30bfd9ab9b|d63722381e0e893d797e4d531e219e2917898364
+
+derby.home=${base.path}/derby-${derby.version}
+derby.jar=${derby.home}/derby-${derby.version}.jar
+derby.loc=${base-maven.loc}/org/apache/derby/derby/${derby.version}/derby-${derby.version}.jar
+derby-shared.jar=${derby.home}/derby-shared-${derby.version}.jar
+derby-shared.loc=${base-maven.loc}/org/apache/derby/derbyshared/${derby.version}/derbyshared-${derby.version}.jar
+derby-tools.jar=${derby.home}/derby-tools-${derby.version}.jar
+derby-tools.loc=${base-maven.loc}/org/apache/derby/derbytools/${derby.version}/derbytools-${derby.version}.jar
+
+
diff --git a/build.xml b/build.xml
index 1a37c0f..34301df 100644
--- a/build.xml
+++ b/build.xml
@@ -260,6 +260,9 @@
     <pathelement location="${cglib.jar}"/>
     <pathelement location="${objenesis.jar}"/>
     <pathelement location="${unboundid.jar}"/>
+    <pathelement location="${derby.jar}"/>
+    <pathelement location="${derby-shared.jar}"/>
+    <pathelement location="${derby-tools.jar}"/>
     <path refid="compile.classpath" />
     <path refid="tomcat.classpath" />
   </path>
@@ -3247,6 +3250,33 @@ skip.installer property in build.properties" />
       <param name="checksum.value" value="${unboundid.checksum.value}"/>
     </antcall>
 
+    <antcall target="downloadfile">
+      <param name="sourcefile" value="${derby.loc}"/>
+      <param name="destfile" value="${derby.jar}"/>
+      <param name="destdir" value="${derby.home}"/>
+      <param name="checksum.enabled" value="${derby.checksum.enabled}"/>
+      <param name="checksum.algorithm" value="${derby.checksum.algorithm}"/>
+      <param name="checksum.value" value="${derby.checksum.value}"/>
+    </antcall>
+
+    <antcall target="downloadfile">
+      <param name="sourcefile" value="${derby-shared.loc}"/>
+      <param name="destfile" value="${derby-shared.jar}"/>
+      <param name="destdir" value="${derby.home}"/>
+      <param name="checksum.enabled" value="${derby-shared.checksum.enabled}"/>
+      <param name="checksum.algorithm" value="${derby-shared.checksum.algorithm}"/>
+      <param name="checksum.value" value="${derby-shared.checksum.value}"/>
+    </antcall>
+
+    <antcall target="downloadfile">
+      <param name="sourcefile" value="${derby-tools.loc}"/>
+      <param name="destfile" value="${derby-tools.jar}"/>
+      <param name="destdir" value="${derby.home}"/>
+      <param name="checksum.enabled" value="${derby-tools.checksum.enabled}"/>
+      <param name="checksum.algorithm" value="${derby-tools.checksum.algorithm}"/>
+      <param name="checksum.value" value="${derby-tools.checksum.value}"/>
+    </antcall>
+
   </target>
 
   <target name="download-cobertura"
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 31e8be9..4b2b413 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -199,6 +199,7 @@
         Update to JSign version 4.0 to enable code signing without the need for
         the installation of additional client tools. (markt)
       </update>
+<<<<<<< HEAD
       <add>
         Update the internal fork of Apache Commons BCEL to 40d5eb4 (2021-09-01,
         6.6.0-SNAPSHOT). Code clean-up only. (markt)
@@ -207,6 +208,12 @@
         Update the internal fork of Apache Commons Codec to fd44e6b (2021-09-01,
         1.16-SNAPSHOT). Minor refactoring. (markt)
       </add>
+=======
+      <update>
+        Add Apache Derby 10.15.2.0 to the testsuite dependencies, for JDBC
+        and DataSource testing. (remm)
+      </update>
+>>>>>>> 86177f0839 (Add Derby for the testsuite)
     </changelog>
   </subsection>
 </section>

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 07/16: Add UserDatabase documentation

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 47b97e0e4a53dcb7d9d5ce8fc8d2b59884b6c8fd
Author: remm <re...@apache.org>
AuthorDate: Fri Aug 27 09:56:32 2021 +0200

    Add UserDatabase documentation
---
 webapps/docs/jndi-resources-howto.xml | 204 +++++++++++++++++++++++++++++++++-
 1 file changed, 202 insertions(+), 2 deletions(-)

diff --git a/webapps/docs/jndi-resources-howto.xml b/webapps/docs/jndi-resources-howto.xml
index 4cdba75..0c682f8 100644
--- a/webapps/docs/jndi-resources-howto.xml
+++ b/webapps/docs/jndi-resources-howto.xml
@@ -428,14 +428,14 @@ public class MyBean2 {
   </subsection>
 
 
-  <subsection name="UserDatabase Resources">
+  <subsection name="Memory UserDatabase Resources">
 
     <h5>0.  Introduction</h5>
 
     <p>UserDatabase resources are typically configured as global resources for
     use by a UserDatabase realm. Tomcat includes a UserDatabaseFactory that
     creates UserDatabase resources backed by an XML file - usually
-    <code>tomcat-users.xml</code></p>
+    <code>tomcat-users.xml</code>.</p>
 
     <p>The steps required to set up a global UserDatabase resource are described
     below.</p>
@@ -496,6 +496,206 @@ public class MyBean2 {
   </subsection>
 
 
+  <subsection name="DataSource UserDatabase Resources">
+
+    <h5>0.  Introduction</h5>
+
+    <p>Tomcat also include a <code>UserDatabase</code> that uses a
+    <code>DataSource</code> resource as the backend. The backend resource
+    must be declared in the same JNDI context as the user database that will use
+    it.</p>
+
+    <p>The steps required to set up a global UserDatabase resource are described
+    below.</p>
+
+    <h5>1. Database schema</h5>
+
+    <p>The database shema for the user database is flexible. It can be the same
+    as the schema used for the <code>DataSourceRealm</code>, with only a table
+    for users (user name, password), and another one listing the roles
+    associated with each user. To support the full <code>UserDatabase</code>
+    features, it must include additional tables for groups, and is
+    compatible with referential integrity between users, groups and roles.</p>
+
+    <p>The full featured schema with groups and referential integrity
+    could be:</p>
+
+<source><![CDATA[create table users (
+  user_name         varchar(32) not null primary key,
+  user_pass         varchar(64) not null,
+  user_fullname     varchar(128)
+  -- Add more attributes as needed
+);
+
+create table roles (
+  role_name         varchar(32) not null primary key,
+  role_description  varchar(128)
+);
+
+create table groups (
+  group_name        varchar(32) not null primary key,
+  group_description varchar(128)
+);
+
+create table user_roles (
+  user_name         varchar(32) references users(user_name),
+  role_name         varchar(32) references roles(role_name),
+  primary key (user_name, role_name)
+);
+
+create table user_groups (
+  user_name         varchar(32) references users(user_name),
+  group_name        varchar(32) references groups(group_name),
+  primary key (user_name, group_name)
+);
+
+create table group_roles (
+  group_name        varchar(32) references groups(group_name),
+  role_name         varchar(32) references roles(role_name),
+  primary key (group_name, role_name)
+);
+]]></source>
+
+    <p>The minimal schema without the ability to use groups will be
+    (it is the same as for the <code>DataSourceRealm</code>):</p>
+
+<source><![CDATA[create table users (
+  user_name         varchar(32) not null primary key,
+  user_pass         varchar(64) not null,
+  -- Add more attributes as needed
+);
+
+create table user_roles (
+  user_name         varchar(32),
+  role_name         varchar(32),
+  primary key (user_name, role_name)
+);
+]]></source>
+
+    <h5>2.  Declare Your Resource</h5>
+
+    <p>Next, modify <code>$CATALINA_BASE/conf/server.xml</code> to create the
+    UserDatabase resource based on your <code>DataSource</code> and its schema.
+    It should look something like this:</p>
+
+<source><![CDATA[<Resource name="UserDatabase" auth="Container"
+              type="org.apache.catalina.UserDatabase"
+              description="User database that can be updated and saved"
+              factory="org.apache.catalina.users.DataSourceUserDatabaseFactory"
+              dataSourceName="jdbc/authority" readonly="false"
+              userTable="users" userNameCol="user_name" userCredCol="user_pass"
+              userRoleTable="user_roles" roleNameCol="role_name"
+              roleTable="roles" groupTable="groups" userGroupTable="user_groups"
+              groupRoleTable="group_roles" groupNameCol="group_name" />]]></source>
+
+    <p>The <code>dataSourceName</code> attribute is the JNDI name of the
+    <code>DataSource</code> that will be the backend for the
+    <code>UserDatabase</code>. It must be declared in the same JNDI
+    <code>Context</code> as the <code>UserDatabase</code>. Please refer to the
+    <a href="#JDBC_Data_Sources"><code>DataSource</code> resources</a>
+    documentation for further instructions.</p>
+
+    <p>The <code>readonly</code> attribute is optional and defaults to
+    <code>true</code> if not supplied. If the database is writeable then changes
+    made through the Tomcat management to the <code>UserDatabase</code> can
+    be persisted to the database using the <code>save</code> operation.</p>
+
+    <p>Alternately, changes can also be made directly to the backend database.
+    </p>
+
+    <h5>3.  Resource configuration</h5>
+
+    <attributes>
+
+      <attribute name="dataSourceName" required="true">
+        <p>The name of the JNDI JDBC DataSource for this UserDatabase.</p>
+      </attribute>
+
+      <attribute name="groupNameCol" required="false">
+        <p>Name of the column, in the "groups", "group roles" and "user groups"
+        tables, that contains the group's name.</p>
+      </attribute>
+
+      <attribute name="groupRoleTable" required="false">
+        <p>Name of the "group roles" table, which must contain columns
+        named by the <code>groupNameCol</code> and <code>roleNameCol</code>
+        attributes.</p>
+      </attribute>
+
+      <attribute name="groupTable" required="false">
+        <p>Name of the "groups" table, which must contain columns named
+        by the <code>groupNameCol</code> attribute.</p>
+      </attribute>
+
+      <attribute name="readonly" required="false">
+        <p>If this is set to <code>true</code>, then changes to the
+        <code>UserDatabase</code> can be persisted to the
+        <code>DataSource</code> by using the <code>save</code> method.
+        The default value is <code>false</code>.</p>
+      </attribute>
+
+      <attribute name="roleAndGroupDescriptionCol" required="false">
+        <p>Name of the column, in the "roles" and "groups" tables, that contains
+        the description for the roles and groups.</p>
+      </attribute>
+
+      <attribute name="roleNameCol" required="true">
+        <p>Name of the column, in the "roles", "user roles" and "group roles"
+        tables, which contains a role name assigned to the corresponding
+        user.</p>
+      </attribute>
+
+      <attribute name="roleTable" required="false">
+        <p>Name of the "roles" table, which must contain columns named
+        by the <code>roleNameCol</code> attribute.</p>
+      </attribute>
+
+      <attribute name="userCredCol" required="true">
+        <p>Name of the column, in the "users" table, which contains
+        the user's credentials (i.e. password).  If a
+        <code>CredentialHandler</code> is specified, this component
+        will assume that the passwords have been encoded with the
+        specified algorithm.  Otherwise, they will be assumed to be
+        in clear text.</p>
+      </attribute>
+
+      <attribute name="userGroupTable" required="false">
+        <p>Name of the "user groups" table, which must contain columns
+        named by the <code>userNameCol</code> and <code>groupNameCol</code>
+        attributes.</p>
+      </attribute>
+
+      <attribute name="userNameCol" required="true">
+        <p>Name of the column, in the "users", "user groups" and "user roles"
+        tables, that contains the user's username.</p>
+      </attribute>
+
+      <attribute name="userFullNameCol" required="false">
+        <p>Name of the column, in the "users" table, that contains the user's
+        full name.</p>
+      </attribute>
+
+      <attribute name="userRoleTable" required="true">
+        <p>Name of the "user roles" table, which must contain columns
+        named by the <code>userNameCol</code> and <code>roleNameCol</code>
+        attributes.</p>
+      </attribute>
+
+      <attribute name="userTable" required="true">
+        <p>Name of the "users" table, which must contain columns named
+        by the <code>userNameCol</code> and <code>userCredCol</code>
+        attributes.</p>
+      </attribute>
+
+    </attributes>
+
+    <h5>4.  Configure the Realm</h5>
+
+    <p>Configure a UserDatabase Realm to use this resource as described in the
+    <a href="config/realm.html">Realm configuration documentation</a>.</p>
+
+  </subsection>
+
   <subsection name="JavaMail Sessions">
 
     <h5>0.  Introduction</h5>

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 01/16: Improve the reusability of UserDatabase code

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 861b4658702c6d79a1ccdf34df1f5a8f47ea90f0
Author: remm <re...@apache.org>
AuthorDate: Tue Aug 17 14:56:24 2021 +0200

    Improve the reusability of UserDatabase code
    
    Add intermediate concrete implementation classes. Add hooks to allow
    partail database updates on save. Will likely make alternate
    implementations more realistic.
---
 java/org/apache/catalina/UserDatabase.java         |  27 ++++
 .../users/{MemoryGroup.java => GenericGroup.java}  |  60 ++++----
 .../users/{MemoryRole.java => GenericRole.java}    |  44 +++---
 .../users/{MemoryUser.java => GenericUser.java}    | 126 ++++++++--------
 java/org/apache/catalina/users/MemoryGroup.java    | 116 +--------------
 java/org/apache/catalina/users/MemoryRole.java     |  36 +----
 java/org/apache/catalina/users/MemoryUser.java     | 158 +--------------------
 webapps/docs/changelog.xml                         |   5 +
 8 files changed, 138 insertions(+), 434 deletions(-)

diff --git a/java/org/apache/catalina/UserDatabase.java b/java/org/apache/catalina/UserDatabase.java
index 8bac810..9242170 100644
--- a/java/org/apache/catalina/UserDatabase.java
+++ b/java/org/apache/catalina/UserDatabase.java
@@ -156,6 +156,33 @@ public interface UserDatabase {
 
 
     /**
+     * Signal the specified {@link Group} from this user database has been
+     * modified.
+     *
+     * @param group The group that has been modified
+     */
+    public default void modifiedGroup(Group group) {}
+
+
+    /**
+     * Signal the specified {@link Role} from this user database has been
+     * modified.
+     *
+     * @param role The role that has been modified
+     */
+    public default void modifiedRole(Role role) {}
+
+
+    /**
+     * Signal the specified {@link User} from this user database has been
+     * modified.
+     *
+     * @param user The user that has been modified
+     */
+    public default void modifiedUser(User user) {}
+
+
+    /**
      * Save any updated information to the persistent storage location for this
      * user database.
      *
diff --git a/java/org/apache/catalina/users/MemoryGroup.java b/java/org/apache/catalina/users/GenericGroup.java
similarity index 73%
copy from java/org/apache/catalina/users/MemoryGroup.java
copy to java/org/apache/catalina/users/GenericGroup.java
index aad4180..d59e57d 100644
--- a/java/org/apache/catalina/users/MemoryGroup.java
+++ b/java/org/apache/catalina/users/GenericGroup.java
@@ -25,17 +25,15 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import org.apache.catalina.Role;
 import org.apache.catalina.User;
 import org.apache.catalina.UserDatabase;
-import org.apache.tomcat.util.buf.StringUtils;
 
 
 /**
- * <p>Concrete implementation of {@link org.apache.catalina.Group} for the
- * {@link MemoryUserDatabase} implementation of {@link UserDatabase}.</p>
+ * <p>Concrete implementation of {@link org.apache.catalina.Group} for a
+ * {@link UserDatabase}.</p>
  *
  * @author Craig R. McClanahan
- * @since 4.1
  */
-public class MemoryGroup extends AbstractGroup {
+public class GenericGroup<UD extends UserDatabase> extends AbstractGroup {
 
 
     // ----------------------------------------------------------- Constructors
@@ -43,19 +41,23 @@ public class MemoryGroup extends AbstractGroup {
 
     /**
      * Package-private constructor used by the factory method in
-     * {@link MemoryUserDatabase}.
+     * {@link UserDatabase}.
      *
-     * @param database The {@link MemoryUserDatabase} that owns this group
+     * @param database The {@link UserDatabase} that owns this group
      * @param groupname Group name of this group
      * @param description Description of this group
+     * @param roles The roles of this group
      */
-    MemoryGroup(MemoryUserDatabase database,
-                String groupname, String description) {
+    GenericGroup(UD database,
+                String groupname, String description, List<Role> roles) {
 
         super();
         this.database = database;
-        setGroupname(groupname);
-        setDescription(description);
+        this.groupname = groupname;
+        this.description = description;
+        if (roles != null) {
+            this.roles.addAll(roles);
+        }
 
     }
 
@@ -64,9 +66,9 @@ public class MemoryGroup extends AbstractGroup {
 
 
     /**
-     * The {@link MemoryUserDatabase} that owns this group.
+     * The {@link UserDatabase} that owns this group.
      */
-    protected final MemoryUserDatabase database;
+    protected final UD database;
 
 
     /**
@@ -123,7 +125,9 @@ public class MemoryGroup extends AbstractGroup {
      */
     @Override
     public void addRole(Role role) {
-        roles.addIfAbsent(role);
+        if (roles.addIfAbsent(role)) {
+            database.modifiedGroup(this);
+        }
     }
 
 
@@ -145,7 +149,9 @@ public class MemoryGroup extends AbstractGroup {
      */
     @Override
     public void removeRole(Role role) {
-        roles.remove(role);
+        if (roles.remove(role)) {
+            database.modifiedGroup(this);
+        }
     }
 
 
@@ -154,27 +160,11 @@ public class MemoryGroup extends AbstractGroup {
      */
     @Override
     public void removeRoles() {
-        roles.clear();
+        if (!roles.isEmpty()) {
+            roles.clear();
+            database.modifiedGroup(this);
+        }
     }
 
 
-    /**
-     * <p>Return a String representation of this group in XML format.</p>
-     */
-    @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder("<group groupname=\"");
-        sb.append(groupname);
-        sb.append("\"");
-        if (description != null) {
-            sb.append(" description=\"");
-            sb.append(description);
-            sb.append("\"");
-        }
-        sb.append(" roles=\"");
-        StringUtils.join(roles, ',', Role::getRolename, sb);
-        sb.append("\"");
-        sb.append("/>");
-        return sb.toString();
-    }
 }
diff --git a/java/org/apache/catalina/users/MemoryRole.java b/java/org/apache/catalina/users/GenericRole.java
similarity index 63%
copy from java/org/apache/catalina/users/MemoryRole.java
copy to java/org/apache/catalina/users/GenericRole.java
index b8f4970..c6b0eba 100644
--- a/java/org/apache/catalina/users/MemoryRole.java
+++ b/java/org/apache/catalina/users/GenericRole.java
@@ -21,13 +21,12 @@ import org.apache.catalina.UserDatabase;
 
 
 /**
- * <p>Concrete implementation of {@link org.apache.catalina.Role} for the
- * {@link MemoryUserDatabase} implementation of {@link UserDatabase}.</p>
+ * <p>Concrete implementation of {@link org.apache.catalina.Role} for a
+ * {@link UserDatabase}.</p>
  *
  * @author Craig R. McClanahan
- * @since 4.1
  */
-public class MemoryRole extends AbstractRole {
+public class GenericRole<UD extends UserDatabase> extends AbstractRole {
 
 
     // ----------------------------------------------------------- Constructors
@@ -35,19 +34,19 @@ public class MemoryRole extends AbstractRole {
 
     /**
      * Package-private constructor used by the factory method in
-     * {@link MemoryUserDatabase}.
+     * {@link UserDatabase}.
      *
-     * @param database The {@link MemoryUserDatabase} that owns this role
+     * @param database The {@link UserDatabase} that owns this role
      * @param rolename Role name of this role
      * @param description Description of this role
      */
-    MemoryRole(MemoryUserDatabase database,
+    GenericRole(UD database,
                String rolename, String description) {
 
         super();
         this.database = database;
-        setRolename(rolename);
-        setDescription(description);
+        this.rolename = rolename;
+        this.description = description;
 
     }
 
@@ -56,9 +55,9 @@ public class MemoryRole extends AbstractRole {
 
 
     /**
-     * The {@link MemoryUserDatabase} that owns this role.
+     * The {@link UserDatabase} that owns this role.
      */
-    protected final MemoryUserDatabase database;
+    protected final UserDatabase database;
 
 
     // ------------------------------------------------------------- Properties
@@ -73,24 +72,17 @@ public class MemoryRole extends AbstractRole {
     }
 
 
-    // --------------------------------------------------------- Public Methods
+    @Override
+    public void setDescription(String description) {
+        database.modifiedRole(this);
+        super.setDescription(description);
+    }
 
 
-    /**
-     * <p>Return a String representation of this role in XML format.</p>
-     */
     @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder("<role rolename=\"");
-        sb.append(rolename);
-        sb.append("\"");
-        if (description != null) {
-            sb.append(" description=\"");
-            sb.append(description);
-            sb.append("\"");
-        }
-        sb.append("/>");
-        return sb.toString();
+    public void setRolename(String rolename) {
+        database.modifiedRole(this);
+        super.setRolename(rolename);
     }
 
 
diff --git a/java/org/apache/catalina/users/MemoryUser.java b/java/org/apache/catalina/users/GenericUser.java
similarity index 62%
copy from java/org/apache/catalina/users/MemoryUser.java
copy to java/org/apache/catalina/users/GenericUser.java
index 007dd56..77b70b4 100644
--- a/java/org/apache/catalina/users/MemoryUser.java
+++ b/java/org/apache/catalina/users/GenericUser.java
@@ -18,22 +18,20 @@ package org.apache.catalina.users;
 
 
 import java.util.Iterator;
+import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.catalina.Group;
 import org.apache.catalina.Role;
 import org.apache.catalina.UserDatabase;
-import org.apache.tomcat.util.buf.StringUtils;
-import org.apache.tomcat.util.security.Escape;
 
 /**
- * <p>Concrete implementation of {@link org.apache.catalina.User} for the
- * {@link MemoryUserDatabase} implementation of {@link UserDatabase}.</p>
+ * <p>Concrete implementation of {@link org.apache.catalina.User} for a
+ * {@link UserDatabase}.</p>
  *
  * @author Craig R. McClanahan
- * @since 4.1
  */
-public class MemoryUser extends AbstractUser {
+public class GenericUser<UD extends UserDatabase> extends AbstractUser {
 
 
     // ----------------------------------------------------------- Constructors
@@ -41,21 +39,30 @@ public class MemoryUser extends AbstractUser {
 
     /**
      * Package-private constructor used by the factory method in
-     * {@link MemoryUserDatabase}.
+     * {@link UserDatabase}.
      *
-     * @param database The {@link MemoryUserDatabase} that owns this user
+     * @param database The {@link UserDatabase} that owns this user
      * @param username Logon username of the new user
      * @param password Logon password of the new user
      * @param fullName Full name of the new user
+     * @param groups The groups of this user
+     * @param roles The roles of this user
      */
-    MemoryUser(MemoryUserDatabase database, String username,
-               String password, String fullName) {
+    GenericUser(UD database, String username,
+               String password, String fullName, List<Group> groups,
+               List<Role> roles) {
 
         super();
         this.database = database;
-        setUsername(username);
-        setPassword(password);
-        setFullName(fullName);
+        this.username = username;
+        this.password = password;
+        this.fullName = fullName;
+        if (groups != null) {
+            this.groups.addAll(groups);
+        }
+        if (roles != null) {
+            this.roles.addAll(roles);
+        }
 
     }
 
@@ -64,9 +71,9 @@ public class MemoryUser extends AbstractUser {
 
 
     /**
-     * The {@link MemoryUserDatabase} that owns this user.
+     * The {@link UserDatabase} that owns this user.
      */
-    protected final MemoryUserDatabase database;
+    protected final UD database;
 
 
     /**
@@ -121,7 +128,9 @@ public class MemoryUser extends AbstractUser {
      */
     @Override
     public void addGroup(Group group) {
-        groups.addIfAbsent(group);
+        if (groups.addIfAbsent(group)) {
+            database.modifiedUser(this);
+        }
     }
 
 
@@ -132,7 +141,9 @@ public class MemoryUser extends AbstractUser {
      */
     @Override
     public void addRole(Role role) {
-        roles.addIfAbsent(role);
+        if (roles.addIfAbsent(role)) {
+            database.modifiedUser(this);
+        }
     }
 
 
@@ -167,7 +178,9 @@ public class MemoryUser extends AbstractUser {
      */
     @Override
     public void removeGroup(Group group) {
-        groups.remove(group);
+        if (groups.remove(group)) {
+            database.modifiedUser(this);
+        }
     }
 
 
@@ -176,7 +189,10 @@ public class MemoryUser extends AbstractUser {
      */
     @Override
     public void removeGroups() {
-        groups.clear();
+        if (!groups.isEmpty()) {
+            groups.clear();
+            database.modifiedUser(this);
+        }
     }
 
 
@@ -187,7 +203,9 @@ public class MemoryUser extends AbstractUser {
      */
     @Override
     public void removeRole(Role role) {
-        roles.remove(role);
+        if (roles.remove(role)) {
+            database.modifiedUser(this);
+        }
     }
 
 
@@ -196,62 +214,32 @@ public class MemoryUser extends AbstractUser {
      */
     @Override
     public void removeRoles() {
+        if (!roles.isEmpty()) {
+            database.modifiedUser(this);
+        }
         roles.clear();
     }
 
 
-    /**
-     * <p>Return a String representation of this user in XML format.</p>
-     *
-     * <p><strong>IMPLEMENTATION NOTE</strong> - For backwards compatibility,
-     * the reader that processes this entry will accept either
-     * <code>username</code> or <code>name</code> for the username
-     * property.</p>
-     * @return the XML representation
-     */
-    public String toXml() {
-
-        StringBuilder sb = new StringBuilder("<user username=\"");
-        sb.append(Escape.xml(username));
-        sb.append("\" password=\"");
-        sb.append(Escape.xml(password));
-        sb.append("\"");
-        if (fullName != null) {
-            sb.append(" fullName=\"");
-            sb.append(Escape.xml(fullName));
-            sb.append("\"");
-        }
-        sb.append(" groups=\"");
-        StringUtils.join(groups, ',', (x) -> Escape.xml(x.getGroupname()), sb);
-        sb.append("\"");
-        sb.append(" roles=\"");
-        StringUtils.join(roles, ',', (x) -> Escape.xml(x.getRolename()), sb);
-        sb.append("\"");
-        sb.append("/>");
-        return sb.toString();
+    @Override
+    public void setFullName(String fullName) {
+        database.modifiedUser(this);
+        super.setFullName(fullName);
     }
 
 
-    /**
-     * <p>Return a String representation of this user.</p>
-     */
     @Override
-    public String toString() {
-
-        StringBuilder sb = new StringBuilder("User username=\"");
-        sb.append(Escape.xml(username));
-        sb.append("\"");
-        if (fullName != null) {
-            sb.append(", fullName=\"");
-            sb.append(Escape.xml(fullName));
-            sb.append("\"");
-        }
-        sb.append(", groups=\"");
-        StringUtils.join(groups, ',', (x) -> Escape.xml(x.getGroupname()), sb);
-        sb.append("\"");
-        sb.append(", roles=\"");
-        StringUtils.join(roles, ',', (x) -> Escape.xml(x.getRolename()), sb);
-        sb.append("\"");
-        return sb.toString();
+    public void setPassword(String password) {
+        database.modifiedUser(this);
+        super.setPassword(password);
     }
+
+
+    @Override
+    public void setUsername(String username) {
+        database.modifiedUser(this);
+        // Note: changing the user name is a problem ...
+        super.setUsername(username);
+    }
+
 }
diff --git a/java/org/apache/catalina/users/MemoryGroup.java b/java/org/apache/catalina/users/MemoryGroup.java
index aad4180..f1008ff 100644
--- a/java/org/apache/catalina/users/MemoryGroup.java
+++ b/java/org/apache/catalina/users/MemoryGroup.java
@@ -17,13 +17,7 @@
 package org.apache.catalina.users;
 
 
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
 import org.apache.catalina.Role;
-import org.apache.catalina.User;
 import org.apache.catalina.UserDatabase;
 import org.apache.tomcat.util.buf.StringUtils;
 
@@ -35,10 +29,7 @@ import org.apache.tomcat.util.buf.StringUtils;
  * @author Craig R. McClanahan
  * @since 4.1
  */
-public class MemoryGroup extends AbstractGroup {
-
-
-    // ----------------------------------------------------------- Constructors
+public class MemoryGroup extends GenericGroup<MemoryUserDatabase> {
 
 
     /**
@@ -51,110 +42,7 @@ public class MemoryGroup extends AbstractGroup {
      */
     MemoryGroup(MemoryUserDatabase database,
                 String groupname, String description) {
-
-        super();
-        this.database = database;
-        setGroupname(groupname);
-        setDescription(description);
-
-    }
-
-
-    // ----------------------------------------------------- Instance Variables
-
-
-    /**
-     * The {@link MemoryUserDatabase} that owns this group.
-     */
-    protected final MemoryUserDatabase database;
-
-
-    /**
-     * The set of {@link Role}s associated with this group.
-     */
-    protected final CopyOnWriteArrayList<Role> roles = new CopyOnWriteArrayList<>();
-
-
-    // ------------------------------------------------------------- Properties
-
-
-    /**
-     * Return the set of {@link Role}s assigned specifically to this group.
-     */
-    @Override
-    public Iterator<Role> getRoles() {
-        return roles.iterator();
-    }
-
-
-    /**
-     * Return the {@link UserDatabase} within which this Group is defined.
-     */
-    @Override
-    public UserDatabase getUserDatabase() {
-        return this.database;
-    }
-
-
-    /**
-     * Return the set of {@link org.apache.catalina.User}s that are members of this group.
-     */
-    @Override
-    public Iterator<User> getUsers() {
-        List<User> results = new ArrayList<>();
-        Iterator<User> users = database.getUsers();
-        while (users.hasNext()) {
-            User user = users.next();
-            if (user.isInGroup(this)) {
-                results.add(user);
-            }
-        }
-        return results.iterator();
-    }
-
-
-    // --------------------------------------------------------- Public Methods
-
-
-    /**
-     * Add a new {@link Role} to those assigned specifically to this group.
-     *
-     * @param role The new role
-     */
-    @Override
-    public void addRole(Role role) {
-        roles.addIfAbsent(role);
-    }
-
-
-    /**
-     * Is this group specifically assigned the specified {@link Role}?
-     *
-     * @param role The role to check
-     */
-    @Override
-    public boolean isInRole(Role role) {
-        return roles.contains(role);
-    }
-
-
-    /**
-     * Remove a {@link Role} from those assigned to this group.
-     *
-     * @param role The old role
-     */
-    @Override
-    public void removeRole(Role role) {
-        roles.remove(role);
-    }
-
-
-    /**
-     * Remove all {@link Role}s from those assigned to this group.
-     */
-    @Override
-    public void removeRoles() {
-        roles.clear();
+        super(database, groupname, description, null);
     }
 
 
diff --git a/java/org/apache/catalina/users/MemoryRole.java b/java/org/apache/catalina/users/MemoryRole.java
index b8f4970..10f6d22 100644
--- a/java/org/apache/catalina/users/MemoryRole.java
+++ b/java/org/apache/catalina/users/MemoryRole.java
@@ -27,10 +27,7 @@ import org.apache.catalina.UserDatabase;
  * @author Craig R. McClanahan
  * @since 4.1
  */
-public class MemoryRole extends AbstractRole {
-
-
-    // ----------------------------------------------------------- Constructors
+public class MemoryRole extends GenericRole<MemoryUserDatabase> {
 
 
     /**
@@ -43,39 +40,10 @@ public class MemoryRole extends AbstractRole {
      */
     MemoryRole(MemoryUserDatabase database,
                String rolename, String description) {
-
-        super();
-        this.database = database;
-        setRolename(rolename);
-        setDescription(description);
-
-    }
-
-
-    // ----------------------------------------------------- Instance Variables
-
-
-    /**
-     * The {@link MemoryUserDatabase} that owns this role.
-     */
-    protected final MemoryUserDatabase database;
-
-
-    // ------------------------------------------------------------- Properties
-
-
-    /**
-     * Return the {@link UserDatabase} within which this role is defined.
-     */
-    @Override
-    public UserDatabase getUserDatabase() {
-        return this.database;
+        super(database, rolename, description);
     }
 
 
-    // --------------------------------------------------------- Public Methods
-
-
     /**
      * <p>Return a String representation of this role in XML format.</p>
      */
diff --git a/java/org/apache/catalina/users/MemoryUser.java b/java/org/apache/catalina/users/MemoryUser.java
index 007dd56..f271fb2 100644
--- a/java/org/apache/catalina/users/MemoryUser.java
+++ b/java/org/apache/catalina/users/MemoryUser.java
@@ -17,11 +17,6 @@
 package org.apache.catalina.users;
 
 
-import java.util.Iterator;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import org.apache.catalina.Group;
-import org.apache.catalina.Role;
 import org.apache.catalina.UserDatabase;
 import org.apache.tomcat.util.buf.StringUtils;
 import org.apache.tomcat.util.security.Escape;
@@ -33,10 +28,7 @@ import org.apache.tomcat.util.security.Escape;
  * @author Craig R. McClanahan
  * @since 4.1
  */
-public class MemoryUser extends AbstractUser {
-
-
-    // ----------------------------------------------------------- Constructors
+public class MemoryUser extends GenericUser<MemoryUserDatabase> {
 
 
     /**
@@ -50,153 +42,7 @@ public class MemoryUser extends AbstractUser {
      */
     MemoryUser(MemoryUserDatabase database, String username,
                String password, String fullName) {
-
-        super();
-        this.database = database;
-        setUsername(username);
-        setPassword(password);
-        setFullName(fullName);
-
-    }
-
-
-    // ----------------------------------------------------- Instance Variables
-
-
-    /**
-     * The {@link MemoryUserDatabase} that owns this user.
-     */
-    protected final MemoryUserDatabase database;
-
-
-    /**
-     * The set of {@link Group}s that this user is a member of.
-     */
-    protected final CopyOnWriteArrayList<Group> groups = new CopyOnWriteArrayList<>();
-
-
-    /**
-     * The set of {@link Role}s associated with this user.
-     */
-    protected final CopyOnWriteArrayList<Role> roles = new CopyOnWriteArrayList<>();
-
-
-    // ------------------------------------------------------------- Properties
-
-
-    /**
-     * Return the set of {@link Group}s to which this user belongs.
-     */
-    @Override
-    public Iterator<Group> getGroups() {
-        return groups.iterator();
-    }
-
-
-    /**
-     * Return the set of {@link Role}s assigned specifically to this user.
-     */
-    @Override
-    public Iterator<Role> getRoles() {
-        return roles.iterator();
-    }
-
-
-    /**
-     * Return the {@link UserDatabase} within which this User is defined.
-     */
-    @Override
-    public UserDatabase getUserDatabase() {
-        return this.database;
-    }
-
-
-    // --------------------------------------------------------- Public Methods
-
-
-    /**
-     * Add a new {@link Group} to those this user belongs to.
-     *
-     * @param group The new group
-     */
-    @Override
-    public void addGroup(Group group) {
-        groups.addIfAbsent(group);
-    }
-
-
-    /**
-     * Add a new {@link Role} to those assigned specifically to this user.
-     *
-     * @param role The new role
-     */
-    @Override
-    public void addRole(Role role) {
-        roles.addIfAbsent(role);
-    }
-
-
-    /**
-     * Is this user in the specified group?
-     *
-     * @param group The group to check
-     */
-    @Override
-    public boolean isInGroup(Group group) {
-        return groups.contains(group);
-    }
-
-
-    /**
-     * Is this user specifically assigned the specified {@link Role}?  This
-     * method does <strong>NOT</strong> check for roles inherited based on
-     * {@link Group} membership.
-     *
-     * @param role The role to check
-     */
-    @Override
-    public boolean isInRole(Role role) {
-        return roles.contains(role);
-    }
-
-
-    /**
-     * Remove a {@link Group} from those this user belongs to.
-     *
-     * @param group The old group
-     */
-    @Override
-    public void removeGroup(Group group) {
-        groups.remove(group);
-    }
-
-
-    /**
-     * Remove all {@link Group}s from those this user belongs to.
-     */
-    @Override
-    public void removeGroups() {
-        groups.clear();
-    }
-
-
-    /**
-     * Remove a {@link Role} from those assigned to this user.
-     *
-     * @param role The old role
-     */
-    @Override
-    public void removeRole(Role role) {
-        roles.remove(role);
-    }
-
-
-    /**
-     * Remove all {@link Role}s from those assigned to this user.
-     */
-    @Override
-    public void removeRoles() {
-        roles.clear();
+        super(database, username, password, fullName, null, null);
     }
 
 
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index d1cf5b1..d8eb83b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -133,6 +133,11 @@
         scheme specific check can be performed. Based on pull request
         <pr>444</pr> by Robert Rodewald. (markt)
       </scode>
+      <update>
+        Improve the reusability of the <code>UserDatabase</code> by adding
+        intermediate concrete implementation classes and allowing to do
+        partial database updates on <code>save</code>. (remm)
+      </update>
     </changelog>
   </subsection>
   <subsection name="Coyote">

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 12/16: Fix group roles not working

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit a25cb20de815339c7d1edb4c6009d7705eed3fe8
Author: remm <re...@apache.org>
AuthorDate: Tue Aug 31 21:34:41 2021 +0200

    Fix group roles not working
    
    Also for a simple schema, the roles didn't work properly. Now when
    there's no role table, roles exist when they are assigned to a user.
---
 .../catalina/users/DataSourceUserDatabase.java     | 26 +++++++++++++++-------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/java/org/apache/catalina/users/DataSourceUserDatabase.java b/java/org/apache/catalina/users/DataSourceUserDatabase.java
index a5bff3a..2c6eb5e 100644
--- a/java/org/apache/catalina/users/DataSourceUserDatabase.java
+++ b/java/org/apache/catalina/users/DataSourceUserDatabase.java
@@ -128,7 +128,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
     /**
      * The generated string for the groups PreparedStatement
      */
-    private String preparedGroupsR = null;
+    private String preparedGroupRoles = null;
 
 
     /**
@@ -640,7 +640,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                         ArrayList<Role> groupRoles = new ArrayList<>();
                         if (groupName != null) {
                             groupName = groupName.trim();
-                            try (PreparedStatement stmt2 = dbConnection.prepareStatement(preparedGroupsR)) {
+                            try (PreparedStatement stmt2 = dbConnection.prepareStatement(preparedGroupRoles)) {
                                 stmt2.setString(1, groupName);
                                 try (ResultSet rs2 = stmt2.executeQuery()) {
                                     while (rs2.next()) {
@@ -869,7 +869,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
             temp.append(" = ?");
             preparedRoles = temp.toString();
 
-            if (userGroupTable != null && userGroupTable.length() > 0) {
+            if (userGroupTable != null) {
                 temp = new StringBuilder("SELECT ");
                 temp.append(groupNameCol);
                 temp.append(" FROM ");
@@ -880,15 +880,15 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                 preparedGroups = temp.toString();
             }
 
-            if (groupRoleTable != null && groupRoleTable.length() > 0) {
+            if (groupRoleTable != null) {
                 temp = new StringBuilder("SELECT ");
-                temp.append(groupNameCol);
+                temp.append(roleNameCol);
                 temp.append(" FROM ");
                 temp.append(groupRoleTable);
                 temp.append(" WHERE ");
                 temp.append(groupNameCol);
                 temp.append(" = ?");
-                preparedGroupsR = temp.toString();
+                preparedGroupRoles = temp.toString();
             }
 
             temp = new StringBuilder("SELECT ");
@@ -909,7 +909,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
             temp.append(userTable);
             preparedAllUsers = temp.toString();
 
-            if (groupTable != null && groupTable.length() > 0) {
+            if (groupTable != null) {
                 temp = new StringBuilder("SELECT ");
                 temp.append(groupNameCol);
                 if (roleAndGroupDescriptionCol != null) {
@@ -929,7 +929,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                 preparedAllGroups = temp.toString();
             }
 
-            if (roleTable != null && roleTable.length() > 0) {
+            if (roleTable != null) {
                 // Create the role PreparedStatement string
                 temp = new StringBuilder("SELECT ");
                 temp.append(roleNameCol);
@@ -948,6 +948,16 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                 temp.append(" FROM ");
                 temp.append(roleTable);
                 preparedAllRoles = temp.toString();
+            } else {
+                // Validate roles existence from the user <-> roles table
+                temp = new StringBuilder("SELECT ");
+                temp.append(roleNameCol);
+                temp.append(" FROM ");
+                temp.append(userRoleTable);
+                temp.append(" WHERE ");
+                temp.append(roleNameCol);
+                temp.append(" = ?");
+                preparedRole = temp.toString();
             }
 
         } finally {

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 04/16: Add a UserDatabase implementation based on DataSourceRealm

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 346114b4d43bcf315ded509c2ed5ebd099c95d3c
Author: remm <re...@apache.org>
AuthorDate: Thu Aug 26 15:44:36 2021 +0200

    Add a UserDatabase implementation based on DataSourceRealm
    
    The goal is to showcase the UserDatabase somewhat more, and this can use
    the minimal schema from the DataSourceRealm with reduced functionality.
    The MBeans are different. The problem with the current behavior is that
    the whole database ends up being registered as individual MBeans.
    There's a new MBean that only uses operations. The MBeans from the
    memory UserDatabase are still there, but they are only populated on
    demand (however, guis often pull the attributes that populate them) and
    they are unregistered when saving back.
    Make related changes to the realm and its custom principal so that the
    results are as dynamic as expected. Add an option for things to be more
    static.
    Will look into the ReentrantReadWriteLock that is used in the memory
    UserDatabase.
    The documentation itself will be straightforward.
---
 .../mbeans/DataSourceUserDatabaseMBean.java        |  360 +++++
 .../apache/catalina/mbeans/LocalStrings.properties |    1 +
 java/org/apache/catalina/mbeans/MBeanUtils.java    |   30 +
 .../catalina/mbeans/SparseUserDatabaseMBean.java   |  369 +++++
 .../apache/catalina/realm/UserDatabaseRealm.java   |   63 +-
 .../catalina/users/DataSourceUserDatabase.java     | 1405 ++++++++++++++++++++
 .../users/DataSourceUserDatabaseFactory.java       |  163 +++
 java/org/apache/catalina/users/GenericGroup.java   |   10 +
 java/org/apache/catalina/users/GenericRole.java    |   10 +
 java/org/apache/catalina/users/GenericUser.java    |   10 +
 .../apache/catalina/users/LocalStrings.properties  |    2 +
 .../apache/catalina/users/SparseUserDatabase.java  |   29 +
 .../apache/catalina/users/mbeans-descriptors.xml   |  346 +++++
 webapps/docs/changelog.xml                         |   10 +
 webapps/docs/config/realm.xml                      |    8 +
 15 files changed, 2813 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java b/java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java
new file mode 100644
index 0000000..86f78ad
--- /dev/null
+++ b/java/org/apache/catalina/mbeans/DataSourceUserDatabaseMBean.java
@@ -0,0 +1,360 @@
+/*
+ * 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.mbeans;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.catalina.Group;
+import org.apache.catalina.Role;
+import org.apache.catalina.User;
+import org.apache.catalina.UserDatabase;
+import org.apache.tomcat.util.modeler.BaseModelMBean;
+import org.apache.tomcat.util.modeler.ManagedBean;
+import org.apache.tomcat.util.modeler.Registry;
+
+/**
+ * <p>A <strong>ModelMBean</strong> implementation for the
+ * <code>org.apache.catalina.users.DataSourceUserDatabase</code> component.</p>
+ *
+ * @author Craig R. McClanahan
+ */
+public class DataSourceUserDatabaseMBean extends BaseModelMBean {
+
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * The configuration information registry for our managed beans.
+     */
+    protected final Registry registry = MBeanUtils.createRegistry();
+
+
+    /**
+     * The <code>ManagedBean</code> information describing this MBean.
+     */
+    protected final ManagedBean managed = registry.findManagedBean("DataSourceUserDatabase");
+
+
+    // ------------------------------------------------------------- Attributes
+
+    /**
+     * @return the names of all groups defined in this database.
+     */
+    public String[] getGroups() {
+        UserDatabase database = (UserDatabase) this.resource;
+        List<String> results = new ArrayList<>();
+        Iterator<Group> groups = database.getGroups();
+        while (groups.hasNext()) {
+            Group group = groups.next();
+            results.add(group.getGroupname());
+        }
+        return results.toArray(new String[0]);
+    }
+
+
+    /**
+     * @return the names of all roles defined in this database.
+     */
+    public String[] getRoles() {
+        UserDatabase database = (UserDatabase) this.resource;
+        List<String> results = new ArrayList<>();
+        Iterator<Role> roles = database.getRoles();
+        while (roles.hasNext()) {
+            Role role = roles.next();
+            results.add(role.getRolename());
+        }
+        return results.toArray(new String[0]);
+    }
+
+
+    /**
+     * @return the names of all users defined in this database.
+     */
+    public String[] getUsers() {
+        UserDatabase database = (UserDatabase) this.resource;
+        List<String> results = new ArrayList<>();
+        Iterator<User> users = database.getUsers();
+        while (users.hasNext()) {
+            User user = users.next();
+            results.add(user.getUsername());
+        }
+        return results.toArray(new String[0]);
+    }
+
+
+    // ------------------------------------------------------------- Operations
+
+    /**
+     * Create a new Group and return the corresponding name.
+     *
+     * @param groupname Group name of the new group
+     * @param description Description of the new group
+     * @return the new group name
+     */
+    public String createGroup(String groupname, String description) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.createGroup(groupname, description);
+        return group.getGroupname();
+    }
+
+
+    /**
+     * Create a new Role and return the corresponding name.
+     *
+     * @param rolename Group name of the new group
+     * @param description Description of the new group
+     * @return the new role name
+     */
+    public String createRole(String rolename, String description) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Role role = database.createRole(rolename, description);
+        return role.getRolename();
+    }
+
+
+    /**
+     * Create a new User and return the corresponding name.
+     *
+     * @param username User name of the new user
+     * @param password Password for the new user
+     * @param fullName Full name for the new user
+     * @return the new user name
+     */
+    public String createUser(String username, String password, String fullName) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.createUser(username, password, fullName);
+        return user.getUsername();
+    }
+
+
+    /**
+     * Remove an existing group.
+     *
+     * @param groupname Group name to remove
+     */
+    public void removeGroup(String groupname) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.findGroup(groupname);
+        if (group == null) {
+            return;
+        }
+        database.removeGroup(group);
+    }
+
+
+    /**
+     * Remove an existing role.
+     *
+     * @param rolename Role name to remove
+     */
+    public void removeRole(String rolename) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Role role = database.findRole(rolename);
+        if (role == null) {
+            return;
+        }
+        database.removeRole(role);
+    }
+
+
+    /**
+     * Remove an existing user.
+     *
+     * @param username User name to remove
+     */
+    public void removeUser(String username) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        if (user == null) {
+            return;
+        }
+        database.removeUser(user);
+    }
+
+
+    /**
+     * Change user credentials.
+     * @param username The user name
+     * @param password The new credentials
+     */
+    public void changeUserPassword(String username, String password) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        if (user != null) {
+            user.setPassword(password);
+        }
+    }
+
+
+    /**
+     * Add specified role to the user.
+     * @param username The user name
+     * @param rolename The role name
+     */
+    public void addUserRole(String username, String rolename) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        Role role = database.findRole(rolename);
+        if (user != null && role != null) {
+            user.addRole(role);
+        }
+    }
+
+
+    /**
+     * Remove specified role from the user.
+     * @param username The user name
+     * @param rolename The role name
+     */
+    public void removeUserRole(String username, String rolename) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        Role role = database.findRole(rolename);
+        if (user != null && role != null) {
+            user.removeRole(role);
+        }
+    }
+
+
+    /**
+     * Get roles for a user.
+     * @param username The user name
+     * @return Array of role names
+     */
+    public String[] getUserRoles(String username) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        if (user != null) {
+            List<String> results = new ArrayList<>();
+            Iterator<Role> roles = user.getRoles();
+            while (roles.hasNext()) {
+                Role role = roles.next();
+                results.add(role.getRolename());
+            }
+            return results.toArray(new String[0]);
+        } else {
+            return null;
+        }
+    }
+
+
+    /**
+     * Add group to user.
+     * @param username The user name
+     * @param groupname The group name
+     */
+    public void addUserGroup(String username, String groupname) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        Group group = database.findGroup(groupname);
+        if (user != null && group != null) {
+            user.addGroup(group);
+        }
+    }
+
+
+    /**
+     * Remove group from user.
+     * @param username The user name
+     * @param groupname The group name
+     */
+    public void removeUserGroup(String username, String groupname) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        Group group = database.findGroup(groupname);
+        if (user != null && group != null) {
+            user.removeGroup(group);
+        }
+    }
+
+
+    /**
+     * Get groups for a user.
+     * @param username The user name
+     * @return Array of group names
+     */
+    public String[] getUserGroups(String username) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        if (user != null) {
+            List<String> results = new ArrayList<>();
+            Iterator<Group> groups = user.getGroups();
+            while (groups.hasNext()) {
+                Group group = groups.next();
+                results.add(group.getGroupname());
+            }
+            return results.toArray(new String[0]);
+        } else {
+            return null;
+        }
+    }
+
+
+    /**
+     * Add role to a group.
+     * @param groupname The group name
+     * @param rolename The role name
+     */
+    public void addGroupRole(String groupname, String rolename) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.findGroup(groupname);
+        Role role = database.findRole(rolename);
+        if (group != null && role != null) {
+            group.addRole(role);
+        }
+    }
+
+
+    /**
+     * Remove role from a group.
+     * @param groupname The group name
+     * @param rolename The role name
+     */
+    public void removeGroupRole(String groupname, String rolename) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.findGroup(groupname);
+        Role role = database.findRole(rolename);
+        if (group != null && role != null) {
+            group.removeRole(role);
+        }
+    }
+
+
+    /**
+     * Get roles for a group.
+     * @param groupname The group name
+     * @return Array of role names
+     */
+    public String[] getGroupRoles(String groupname) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.findGroup(groupname);
+        if (group != null) {
+            List<String> results = new ArrayList<>();
+            Iterator<Role> roles = group.getRoles();
+            while (roles.hasNext()) {
+                Role role = roles.next();
+                results.add(role.getRolename());
+            }
+            return results.toArray(new String[0]);
+        } else {
+            return null;
+        }
+    }
+
+
+}
diff --git a/java/org/apache/catalina/mbeans/LocalStrings.properties b/java/org/apache/catalina/mbeans/LocalStrings.properties
index 88ecbc6..3a844a0 100644
--- a/java/org/apache/catalina/mbeans/LocalStrings.properties
+++ b/java/org/apache/catalina/mbeans/LocalStrings.properties
@@ -69,3 +69,4 @@ userMBean.destroyError.role=Exception destroying role [{0}] MBean
 userMBean.destroyError.user=Exception destroying user [{0}] MBean
 userMBean.invalidGroup=Invalid group name [{0}]
 userMBean.invalidRole=Invalid role name [{0}]
+userMBean.saveError=Error during save operation
diff --git a/java/org/apache/catalina/mbeans/MBeanUtils.java b/java/org/apache/catalina/mbeans/MBeanUtils.java
index 120141e..2ea6603 100644
--- a/java/org/apache/catalina/mbeans/MBeanUtils.java
+++ b/java/org/apache/catalina/mbeans/MBeanUtils.java
@@ -65,6 +65,12 @@ public class MBeanUtils {
           "Role" },
         { "org.apache.catalina.users.MemoryUser",
           "User" },
+        { "org.apache.catalina.users.GenericGroup",
+          "Group" },
+        { "org.apache.catalina.users.GenericRole",
+          "Role" },
+        { "org.apache.catalina.users.GenericUser",
+          "User" }
     };
 
 
@@ -311,6 +317,25 @@ public class MBeanUtils {
     static DynamicMBean createMBean(UserDatabase userDatabase)
         throws Exception {
 
+        if (userDatabase.isSparse()) {
+            // Register a sparse database bean as well
+            ManagedBean managed = registry.findManagedBean("SparseUserDatabase");
+            if (managed == null) {
+                Exception e = new Exception(sm.getString("mBeanUtils.noManagedBean", "SparseUserDatabase"));
+                throw new MBeanException(e);
+            }
+            String domain = managed.getDomain();
+            if (domain == null) {
+                domain = mserver.getDefaultDomain();
+            }
+            DynamicMBean mbean = managed.createMBean(userDatabase);
+            ObjectName oname = createObjectName(domain, userDatabase);
+            if( mserver.isRegistered( oname ))  {
+                mserver.unregisterMBean(oname);
+            }
+            mserver.registerMBean(mbean, oname);
+        }
+
         String mname = createManagedName(userDatabase);
         ManagedBean managed = registry.findManagedBean(mname);
         if (managed == null) {
@@ -804,5 +829,10 @@ public class MBeanUtils {
         if( mserver.isRegistered(db) ) {
             mserver.unregisterMBean(db);
         }
+        db = new ObjectName(
+                "Catalina:type=UserDatabase,database=" + userDatabase);
+        if( mserver.isRegistered(db) ) {
+            mserver.unregisterMBean(db);
+        }
     }
 }
diff --git a/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java b/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java
new file mode 100644
index 0000000..921424d
--- /dev/null
+++ b/java/org/apache/catalina/mbeans/SparseUserDatabaseMBean.java
@@ -0,0 +1,369 @@
+/*
+ * 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.mbeans;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.catalina.Group;
+import org.apache.catalina.Role;
+import org.apache.catalina.User;
+import org.apache.catalina.UserDatabase;
+import org.apache.tomcat.util.modeler.BaseModelMBean;
+import org.apache.tomcat.util.modeler.ManagedBean;
+import org.apache.tomcat.util.modeler.Registry;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * <p>A <strong>ModelMBean</strong> implementation for the
+ * <code>org.apache.catalina.users.SparseUserDatabase</code> component.
+ * The main difference is that the MBeans are created on demand (for exemple,
+ * the findUser method would register the corresponding user and make it
+ * available for management. All the MBeans created for users, groups and roles
+ * are then discarded when save is invoked.</p>
+ *
+ * @author Craig R. McClanahan
+ */
+public class SparseUserDatabaseMBean extends BaseModelMBean {
+
+    private static final StringManager sm = StringManager.getManager(SparseUserDatabaseMBean.class);
+
+    // ----------------------------------------------------- Instance Variables
+
+    /**
+     * The configuration information registry for our managed beans.
+     */
+    protected final Registry registry = MBeanUtils.createRegistry();
+
+
+    /**
+     * The <code>MBeanServer</code> for this application.
+     */
+    protected final MBeanServer mserver = MBeanUtils.createServer();
+
+
+    /**
+     * The <code>ManagedBean</code> information describing this MBean.
+     */
+    protected final ManagedBean managed = registry.findManagedBean("SparseUserDatabase");
+
+
+    /**
+     * The <code>ManagedBean</code> information describing Group MBeans.
+     */
+    protected final ManagedBean managedGroup = registry.findManagedBean("Group");
+
+
+    /**
+     * The <code>ManagedBean</code> information describing Group MBeans.
+     */
+    protected final ManagedBean managedRole = registry.findManagedBean("Role");
+
+
+    /**
+     * The <code>ManagedBean</code> information describing User MBeans.
+     */
+    protected final ManagedBean managedUser = registry.findManagedBean("User");
+
+
+    // ------------------------------------------------------------- Attributes
+
+    /**
+     * @return the MBean Names of all groups defined in this database.
+     */
+    public String[] getGroups() {
+        UserDatabase database = (UserDatabase) this.resource;
+        List<String> results = new ArrayList<>();
+        Iterator<Group> groups = database.getGroups();
+        while (groups.hasNext()) {
+            Group group = groups.next();
+            results.add(findGroup(group.getGroupname()));
+        }
+        return results.toArray(new String[0]);
+    }
+
+
+    /**
+     * @return the MBean Names of all roles defined in this database.
+     */
+    public String[] getRoles() {
+        UserDatabase database = (UserDatabase) this.resource;
+        List<String> results = new ArrayList<>();
+        Iterator<Role> roles = database.getRoles();
+        while (roles.hasNext()) {
+            Role role = roles.next();
+            results.add(findRole(role.getRolename()));
+        }
+        return results.toArray(new String[0]);
+    }
+
+
+    /**
+     * @return the MBean Names of all users defined in this database.
+     */
+    public String[] getUsers() {
+        UserDatabase database = (UserDatabase) this.resource;
+        List<String> results = new ArrayList<>();
+        Iterator<User> users = database.getUsers();
+        while (users.hasNext()) {
+            User user = users.next();
+            results.add(findUser(user.getUsername()));
+        }
+        return results.toArray(new String[0]);
+    }
+
+
+    // ------------------------------------------------------------- Operations
+
+    /**
+     * Create a new Group and return the corresponding MBean Name.
+     *
+     * @param groupname Group name of the new group
+     * @param description Description of the new group
+     * @return the new group object name
+     */
+    public String createGroup(String groupname, String description) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.createGroup(groupname, description);
+        try {
+            MBeanUtils.createMBean(group);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.group", groupname), e);
+        }
+        return findGroup(groupname);
+    }
+
+
+    /**
+     * Create a new Role and return the corresponding MBean Name.
+     *
+     * @param rolename Group name of the new group
+     * @param description Description of the new group
+     * @return the new role object name
+     */
+    public String createRole(String rolename, String description) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Role role = database.createRole(rolename, description);
+        try {
+            MBeanUtils.createMBean(role);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.role", rolename), e);
+        }
+        return findRole(rolename);
+    }
+
+
+    /**
+     * Create a new User and return the corresponding MBean Name.
+     *
+     * @param username User name of the new user
+     * @param password Password for the new user
+     * @param fullName Full name for the new user
+     * @return the new user object name
+     */
+    public String createUser(String username, String password, String fullName) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.createUser(username, password, fullName);
+        try {
+            MBeanUtils.createMBean(user);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.createMBeanError.user", username), e);
+        }
+        return findUser(username);
+    }
+
+
+    /**
+     * Return the MBean Name for the specified group name (if any);
+     * otherwise return <code>null</code>.
+     *
+     * @param groupname Group name to look up
+     * @return the group object name
+     */
+    public String findGroup(String groupname) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.findGroup(groupname);
+        if (group == null) {
+            return null;
+        }
+        try {
+            ObjectName oname = MBeanUtils.createObjectName(managedGroup.getDomain(), group);
+            if (!mserver.isRegistered(oname)) {
+                MBeanUtils.createMBean(group);
+            }
+            return oname.toString();
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.createError.group", groupname), e);
+        }
+    }
+
+
+    /**
+     * Return the MBean Name for the specified role name (if any);
+     * otherwise return <code>null</code>.
+     *
+     * @param rolename Role name to look up
+     * @return the role object name
+     */
+    public String findRole(String rolename) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Role role = database.findRole(rolename);
+        if (role == null) {
+            return null;
+        }
+        try {
+            ObjectName oname = MBeanUtils.createObjectName(managedRole.getDomain(), role);
+            if (!mserver.isRegistered(oname)) {
+                MBeanUtils.createMBean(role);
+            }
+            return oname.toString();
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.createError.role", rolename), e);
+        }
+
+    }
+
+
+    /**
+     * Return the MBean Name for the specified user name (if any);
+     * otherwise return <code>null</code>.
+     *
+     * @param username User name to look up
+     * @return the user object name
+     */
+    public String findUser(String username) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        if (user == null) {
+            return null;
+        }
+        try {
+            ObjectName oname = MBeanUtils.createObjectName(managedUser.getDomain(), user);
+            if (!mserver.isRegistered(oname)) {
+                MBeanUtils.createMBean(user);
+            }
+            return oname.toString();
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.createError.user", username), e);
+        }
+    }
+
+
+    /**
+     * Remove an existing group and destroy the corresponding MBean.
+     *
+     * @param groupname Group name to remove
+     */
+    public void removeGroup(String groupname) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Group group = database.findGroup(groupname);
+        if (group == null) {
+            return;
+        }
+        try {
+            MBeanUtils.destroyMBean(group);
+            database.removeGroup(group);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.destroyError.group", groupname), e);
+        }
+    }
+
+
+    /**
+     * Remove an existing role and destroy the corresponding MBean.
+     *
+     * @param rolename Role name to remove
+     */
+    public void removeRole(String rolename) {
+        UserDatabase database = (UserDatabase) this.resource;
+        Role role = database.findRole(rolename);
+        if (role == null) {
+            return;
+        }
+        try {
+            MBeanUtils.destroyMBean(role);
+            database.removeRole(role);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.destroyError.role", rolename), e);
+        }
+    }
+
+
+    /**
+     * Remove an existing user and destroy the corresponding MBean.
+     *
+     * @param username User name to remove
+     */
+    public void removeUser(String username) {
+        UserDatabase database = (UserDatabase) this.resource;
+        User user = database.findUser(username);
+        if (user == null) {
+            return;
+        }
+        try {
+            MBeanUtils.destroyMBean(user);
+            database.removeUser(user);
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.destroyError.user", username), e);
+        }
+    }
+
+
+    /**
+     * Call actual save and unregister all obsolete beans.
+     */
+    public void save() {
+        try {
+            UserDatabase database = (UserDatabase) this.resource;
+            ObjectName query = null;
+            Set<ObjectName> results = null;
+
+            // Groups
+            query = new ObjectName(
+                    "Users:type=Group,database=" + database.getId() + ",*");
+            results = mserver.queryNames(query, null);
+            for (ObjectName result : results) {
+                mserver.unregisterMBean(result);
+            }
+
+            // Roles
+            query = new ObjectName(
+                    "Users:type=Role,database=" + database.getId() + ",*");
+            results = mserver.queryNames(query, null);
+            for (ObjectName result : results) {
+                mserver.unregisterMBean(result);
+            }
+
+            // Users
+            query = new ObjectName(
+                    "Users:type=User,database=" + database.getId() + ",*");
+            results = mserver.queryNames(query, null);
+            for (ObjectName result : results) {
+                mserver.unregisterMBean(result);
+            }
+
+            database.save();
+        } catch (Exception e) {
+            throw new IllegalArgumentException(sm.getString("userMBean.saveError"), e);
+        }
+    }
+}
diff --git a/java/org/apache/catalina/realm/UserDatabaseRealm.java b/java/org/apache/catalina/realm/UserDatabaseRealm.java
index d98115f..ef072d6 100644
--- a/java/org/apache/catalina/realm/UserDatabaseRealm.java
+++ b/java/org/apache/catalina/realm/UserDatabaseRealm.java
@@ -65,6 +65,13 @@ public class UserDatabaseRealm extends RealmBase {
      */
     private boolean localJndiResource = false;
 
+    /**
+     * Use a static principal disconnected from the database. This prevents live
+     * updates to users and roles having an effect on authenticated principals,
+     * but reduces use of the database.
+     */
+    private boolean useStaticPrincipal = false;
+
 
     // ------------------------------------------------------------- Properties
 
@@ -89,6 +96,23 @@ public class UserDatabaseRealm extends RealmBase {
 
 
     /**
+     * @return the useStaticPrincipal flag
+     */
+    public boolean getUseStaticPrincipal() {
+        return this.useStaticPrincipal;
+    }
+
+
+    /**
+     * Allows using a static principal disconnected from the user database.
+     * @param useStaticPrincipal the new value
+     */
+    public void setUseStaticPrincipal(boolean useStaticPrincipal) {
+        this.useStaticPrincipal = useStaticPrincipal;
+    }
+
+
+    /**
      * Determines whether this Realm is configured to obtain the associated
      * {@link UserDatabase} from the global JNDI context or a local (web
      * application) JNDI context.
@@ -144,6 +168,26 @@ public class UserDatabaseRealm extends RealmBase {
     }
 
 
+    public static String[] getRoles(User user) {
+        Set<String> roles = new HashSet<>();
+        Iterator<Role> uroles = user.getRoles();
+        while (uroles.hasNext()) {
+            Role role = uroles.next();
+            roles.add(role.getName());
+        }
+        Iterator<Group> groups = user.getGroups();
+        while (groups.hasNext()) {
+            Group group = groups.next();
+            uroles = group.getRoles();
+            while (uroles.hasNext()) {
+                Role role = uroles.next();
+                roles.add(role.getName());
+            }
+        }
+        return roles.toArray(new String[0]);
+    }
+
+
     /**
      * Return the Principal associated with the given user name.
      */
@@ -157,7 +201,11 @@ public class UserDatabaseRealm extends RealmBase {
         if (user == null) {
             return null;
         } else {
-            return new UserDatabasePrincipal(user, database);
+            if (useStaticPrincipal) {
+                return new GenericPrincipal(username, Arrays.asList(getRoles(user))); 
+            } else {
+                return new UserDatabasePrincipal(user, database);
+            }
         }
     }
 
@@ -239,17 +287,22 @@ public class UserDatabaseRealm extends RealmBase {
 
     public static final class UserDatabasePrincipal extends GenericPrincipal {
         private static final long serialVersionUID = 1L;
-        private final transient User user;
         private final transient UserDatabase database;
 
         public UserDatabasePrincipal(User user, UserDatabase database) {
             super(user.getName(), null, null);
-            this.user = user;
             this.database = database;
         }
 
         @Override
         public String[] getRoles() {
+            if (database == null) {
+                return new String[0];
+            }
+            User user = database.findUser(name);
+            if (user == null) {
+                return new String[0];
+            }
             Set<String> roles = new HashSet<>();
             Iterator<Role> uroles = user.getRoles();
             while (uroles.hasNext()) {
@@ -282,6 +335,10 @@ public class UserDatabaseRealm extends RealmBase {
             if (dbrole == null) {
                 return false;
             }
+            User user = database.findUser(name);
+            if (user == null) {
+                return false;
+            }
             if (user.isInRole(dbrole)) {
                 return true;
             }
diff --git a/java/org/apache/catalina/users/DataSourceUserDatabase.java b/java/org/apache/catalina/users/DataSourceUserDatabase.java
new file mode 100644
index 0000000..0420e51
--- /dev/null
+++ b/java/org/apache/catalina/users/DataSourceUserDatabase.java
@@ -0,0 +1,1405 @@
+/*
+ *  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.users;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.naming.Context;
+import javax.sql.DataSource;
+
+import org.apache.catalina.Group;
+import org.apache.catalina.Role;
+import org.apache.catalina.User;
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * UserDatabase backed by a data source.
+ */
+public class DataSourceUserDatabase extends SparseUserDatabase {
+
+    private static final Log log = LogFactory.getLog(DataSourceUserDatabase.class);
+    private static final StringManager sm = StringManager.getManager(DataSourceUserDatabase.class);
+
+    public DataSourceUserDatabase(Context namingContext, String id) {
+        this.namingContext = namingContext;
+        this.id = id;
+    }
+
+
+    /**
+     * Associated naming context (will be used to bet the DataSource).
+     */
+    protected final Context namingContext;
+
+
+    /**
+     * The unique global identifier of this user database.
+     */
+    protected final String id;
+
+    protected final ConcurrentHashMap<String, User> createdUsers = new ConcurrentHashMap<>();
+    protected final ConcurrentHashMap<String, User> modifiedUsers = new ConcurrentHashMap<>();
+    protected final ConcurrentHashMap<String, User> removedUsers = new ConcurrentHashMap<>();
+
+    protected final ConcurrentHashMap<String, Group> createdGroups = new ConcurrentHashMap<>();
+    protected final ConcurrentHashMap<String, Group> modifiedGroups = new ConcurrentHashMap<>();
+    protected final ConcurrentHashMap<String, Group> removedGroups = new ConcurrentHashMap<>();
+
+    protected final ConcurrentHashMap<String, Role> createdRoles = new ConcurrentHashMap<>();
+    protected final ConcurrentHashMap<String, Role> modifiedRoles = new ConcurrentHashMap<>();
+    protected final ConcurrentHashMap<String, Role> removedRoles = new ConcurrentHashMap<>();
+
+    // ----------------------------------------------------- Instance Variables
+
+
+    /**
+     * The generated string for the all users PreparedStatement
+     */
+    private String preparedAllUsers = null;
+
+
+    /**
+     * The generated string for the all groups PreparedStatement
+     */
+    private String preparedAllGroups = null;
+
+
+    /**
+     * The generated string for the all roles PreparedStatement
+     */
+    private String preparedAllRoles = null;
+
+
+    /**
+     * The generated string for the group PreparedStatement
+     */
+    private String preparedGroup = null;
+
+
+    /**
+     * The generated string for the role PreparedStatement
+     */
+    private String preparedRole = null;
+
+
+    /**
+     * The generated string for the roles PreparedStatement
+     */
+    private String preparedRoles = null;
+
+
+    /**
+     * The generated string for the user PreparedStatement
+     */
+    private String preparedUser = null;
+
+
+    /**
+     * The generated string for the groups PreparedStatement
+     */
+    private String preparedGroups = null;
+
+
+    /**
+     * The generated string for the groups PreparedStatement
+     */
+    private String preparedGroupsR = null;
+
+
+    /**
+     * The name of the JNDI JDBC DataSource
+     */
+    protected String dataSourceName = null;
+
+
+    /**
+     * The column in the user role table that names a role
+     */
+    protected String roleNameCol = null;
+
+
+    /**
+     * The column in the role and group tables for the decription
+     */
+    protected String roleAndGroupDescriptionCol = null;
+
+
+    /**
+     * The column in the user group table that names a group
+     */
+    protected String groupNameCol = 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 full name
+     */
+    protected String userFullNameCol = null;
+
+
+    /**
+     * The column in the user table that holds the user's name
+     */
+    protected String userNameCol = null;
+
+
+    /**
+     * The table that holds the relation between users and roles
+     */
+    protected String userRoleTable = null;
+
+
+    /**
+     * The table that holds the relation between users and groups
+     */
+    protected String userGroupTable = null;
+
+
+    /**
+     * The table that holds the relation between groups and roles
+     */
+    protected String groupRoleTable = null;
+
+
+    /**
+     * The table that holds user data.
+     */
+    protected String userTable = null;
+
+
+    /**
+     * The table that holds user data.
+     */
+    protected String groupTable = null;
+
+
+    /**
+     * The table that holds user data.
+     */
+    protected String roleTable = null;
+
+
+    /**
+     * Last connection attempt.
+     */
+    private volatile boolean connectionSuccess = true;
+
+
+    /**
+     * A flag, indicating if the user database is read only.
+     */
+    protected boolean readonly = 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 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;
+    }
+
+
+    /**
+     * @return the roleAndGroupDescriptionCol
+     */
+    public String getRoleAndGroupDescriptionCol() {
+        return this.roleAndGroupDescriptionCol;
+    }
+
+    /**
+     * @param roleAndGroupDescriptionCol the roleAndGroupDescriptionCol to set
+     */
+    public void setRoleAndGroupDescriptionCol(String roleAndGroupDescriptionCol) {
+        this.roleAndGroupDescriptionCol = roleAndGroupDescriptionCol;
+    }
+
+    /**
+     * @return the groupNameCol
+     */
+    public String getGroupNameCol() {
+        return this.groupNameCol;
+    }
+
+    /**
+     * @param groupNameCol the groupNameCol to set
+     */
+    public void setGroupNameCol(String groupNameCol) {
+        this.groupNameCol = groupNameCol;
+    }
+
+    /**
+     * @return the userFullNameCol
+     */
+    public String getUserFullNameCol() {
+        return this.userFullNameCol;
+    }
+
+    /**
+     * @param userFullNameCol the userFullNameCol to set
+     */
+    public void setUserFullNameCol(String userFullNameCol) {
+        this.userFullNameCol = userFullNameCol;
+    }
+
+    /**
+     * @return the userGroupTable
+     */
+    public String getUserGroupTable() {
+        return this.userGroupTable;
+    }
+
+    /**
+     * @param userGroupTable the userGroupTable to set
+     */
+    public void setUserGroupTable(String userGroupTable) {
+        this.userGroupTable = userGroupTable;
+    }
+
+    /**
+     * @return the groupRoleTable
+     */
+    public String getGroupRoleTable() {
+        return this.groupRoleTable;
+    }
+
+    /**
+     * @param groupRoleTable the groupRoleTable to set
+     */
+    public void setGroupRoleTable(String groupRoleTable) {
+        this.groupRoleTable = groupRoleTable;
+    }
+
+    /**
+     * @return the groupTable
+     */
+    public String getGroupTable() {
+        return this.groupTable;
+    }
+
+    /**
+     * @param groupTable the groupTable to set
+     */
+    public void setGroupTable(String groupTable) {
+        this.groupTable = groupTable;
+    }
+
+    /**
+     * @return the roleTable
+     */
+    public String getRoleTable() {
+        return this.roleTable;
+    }
+
+    /**
+     * @param roleTable the roleTable to set
+     */
+    public void setRoleTable(String roleTable) {
+        this.roleTable = roleTable;
+    }
+
+    /**
+     * @return the readonly
+     */
+    public boolean getReadonly() {
+        return this.readonly;
+    }
+
+    /**
+     * @param readonly the readonly to set
+     */
+    public void setReadonly(boolean readonly) {
+        this.readonly = readonly;
+    }
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    @Override
+    public Iterator<Group> getGroups() {
+        HashMap<String, Group> groups = new HashMap<>();
+        groups.putAll(createdGroups);
+        groups.putAll(modifiedGroups);
+
+        Connection dbConnection = openConnection();
+        if (dbConnection != null && preparedAllGroups != null) {
+            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllGroups)) {
+                try (ResultSet rs = stmt.executeQuery()) {
+                    while (rs.next()) {
+                        String groupName = rs.getString(1);
+                        if (groupName != null) {
+                            if (!groups.containsKey(groupName) && !removedGroups.containsKey(groupName)) {
+                                Group group = findGroupInternal(dbConnection, groupName);
+                                if (group != null) {
+                                    groups.put(groupName, group);
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (SQLException e) {
+                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+            } finally {
+                close(dbConnection);
+            }
+        }
+
+        return groups.values().iterator();
+    }
+
+    @Override
+    public Iterator<Role> getRoles() {
+        HashMap<String, Role> roles = new HashMap<>();
+        roles.putAll(createdRoles);
+        roles.putAll(modifiedRoles);
+
+        Connection dbConnection = openConnection();
+        if (dbConnection != null && preparedAllRoles != null) {
+            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllRoles)) {
+                try (ResultSet rs = stmt.executeQuery()) {
+                    while (rs.next()) {
+                        String roleName = rs.getString(1);
+                        if (roleName != null) {
+                            if (!roles.containsKey(roleName) && !removedRoles.containsKey(roleName)) {
+                                Role role = findRoleInternal(dbConnection, roleName);
+                                if (role != null) {
+                                    roles.put(roleName, role);
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (SQLException e) {
+                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+            } finally {
+                close(dbConnection);
+            }
+        }
+
+        return roles.values().iterator();
+    }
+
+    @Override
+    public Iterator<User> getUsers() {
+        HashMap<String, User> users = new HashMap<>();
+        users.putAll(createdUsers);
+        users.putAll(modifiedUsers);
+
+        Connection dbConnection = openConnection();
+        if (dbConnection != null) {
+            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedAllUsers)) {
+                try (ResultSet rs = stmt.executeQuery()) {
+                    while (rs.next()) {
+                        String userName = rs.getString(1);
+                        if (userName != null) {
+                            if (!users.containsKey(userName) && !removedUsers.containsKey(userName)) {
+                                User user = findUserInternal(dbConnection, userName);
+                                if (user != null) {
+                                    users.put(userName, user);
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (SQLException e) {
+                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+            } finally {
+                close(dbConnection);
+            }
+        }
+
+        return users.values().iterator();
+    }
+
+    @Override
+    public void close() throws Exception {
+    }
+
+    @Override
+    public Group createGroup(String groupname, String description) {
+        Group group = new GenericGroup<DataSourceUserDatabase>(this, groupname, description, null);
+        createdGroups.put(groupname, group);
+        modifiedGroups.remove(groupname);
+        removedGroups.remove(groupname);
+        return group;
+    }
+
+    @Override
+    public Role createRole(String rolename, String description) {
+        Role role = new GenericRole<DataSourceUserDatabase>(this, rolename, description);
+        createdRoles.put(rolename, role);
+        modifiedRoles.remove(rolename);
+        removedRoles.remove(rolename);
+        return role;
+    }
+
+    @Override
+    public User createUser(String username, String password, String fullName) {
+        User user = new GenericUser<DataSourceUserDatabase>(this, username, password, fullName, null, null);
+        createdUsers.put(username, user);
+        modifiedUsers.remove(username);
+        removedUsers.remove(username);
+        return user;
+    }
+
+    @Override
+    public Group findGroup(String groupname) {
+        // Check local changes first
+        Group group = createdGroups.get(groupname);
+        if (group != null) {
+            return group;
+        }
+        group = modifiedGroups.get(groupname);
+        if (group != null) {
+            return group;
+        }
+        group = removedGroups.get(groupname);
+        if (group != null) {
+            return null;
+        }
+
+        Connection dbConnection = openConnection();
+        if (dbConnection == null) {
+            return null;
+        }
+        try {
+            return findGroupInternal(dbConnection, groupname);
+        } finally {
+            close(dbConnection);
+        }
+    }
+
+    public Group findGroupInternal(Connection dbConnection, String groupName) {
+        Group group = null;
+        try (PreparedStatement stmt = dbConnection.prepareStatement(preparedGroup)) {
+            stmt.setString(1, groupName);
+            try (ResultSet rs = stmt.executeQuery()) {
+                if (rs.next()) {
+                    if (rs.getString(1) != null) {
+                        String description = (roleAndGroupDescriptionCol != null) ? rs.getString(2) : null;
+                        ArrayList<Role> groupRoles = new ArrayList<>();
+                        if (groupName != null) {
+                            groupName = groupName.trim();
+                            try (PreparedStatement stmt2 = dbConnection.prepareStatement(preparedGroupsR)) {
+                                stmt2.setString(1, groupName);
+                                try (ResultSet rs2 = stmt2.executeQuery()) {
+                                    while (rs2.next()) {
+                                        String roleName = rs2.getString(1);
+                                        if (roleName != null) {
+                                            Role groupRole = findRoleInternal(dbConnection, roleName);
+                                            if (groupRole != null) {
+                                                groupRoles.add(groupRole);
+                                            }
+                                        }
+                                    }
+                                }
+                            } catch (SQLException e) {
+                                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                            }
+                        }
+                        group = new GenericGroup<DataSourceUserDatabase>(this, groupName, description, groupRoles);
+                    }
+                }
+            }
+        } catch (SQLException e) {
+            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+        }
+        return group;
+    }
+
+    @Override
+    public Role findRole(String rolename) {
+        // Check local changes first
+        Role role = createdRoles.get(rolename);
+        if (role != null) {
+            return role;
+        }
+        role = modifiedRoles.get(rolename);
+        if (role != null) {
+            return role;
+        }
+        role = removedRoles.get(rolename);
+        if (role != null) {
+            return null;
+        }
+
+        Connection dbConnection = openConnection();
+        if (dbConnection == null) {
+            return null;
+        }
+        try {
+            return findRoleInternal(dbConnection, rolename);
+        } finally {
+            close(dbConnection);
+        }
+    }
+
+    public Role findRoleInternal(Connection dbConnection, String roleName) {
+        Role role = null;
+        try (PreparedStatement stmt = dbConnection.prepareStatement(preparedRole)) {
+            stmt.setString(1, roleName);
+            try (ResultSet rs = stmt.executeQuery()) {
+                if (rs.next()) {
+                    if (rs.getString(1) != null) {
+                        String description = (roleAndGroupDescriptionCol != null) ? rs.getString(2) : null;
+                        role = new GenericRole<DataSourceUserDatabase>(this, roleName, description);
+                    }
+                }
+            }
+        } catch (SQLException e) {
+            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+        }
+        return role;
+    }
+
+    @Override
+    public User findUser(String username) {
+        // Check local changes first
+        User user = createdUsers.get(username);
+        if (user != null) {
+            return user;
+        }
+        user = modifiedUsers.get(username);
+        if (user != null) {
+            return user;
+        }
+        user = removedUsers.get(username);
+        if (user != null) {
+            return null;
+        }
+
+        Connection dbConnection = openConnection();
+        if (dbConnection == null) {
+            return null;
+        }
+        try {
+            return findUserInternal(dbConnection, username);
+        } finally {
+            close(dbConnection);
+        }
+    }
+
+    public User findUserInternal(Connection dbConnection, String userName) {
+        String dbCredentials = null;
+        String fullName = null;
+
+        try (PreparedStatement stmt = dbConnection.prepareStatement(preparedUser)) {
+            stmt.setString(1, userName);
+
+            try (ResultSet rs = stmt.executeQuery()) {
+                if (rs.next()) {
+                    dbCredentials = rs.getString(1);
+                    if (userFullNameCol != null) {
+                        fullName = rs.getString(2);
+                    }
+                }
+
+                dbCredentials = (dbCredentials != null) ? dbCredentials.trim() : null;
+            }
+        } catch (SQLException e) {
+            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+        }
+
+        // Lookup groups
+        ArrayList<Group> groups = new ArrayList<>();
+        if (isGroupStoreDefined()) {
+            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedGroups)) {
+                stmt.setString(1, userName);
+                try (ResultSet rs = stmt.executeQuery()) {
+                    while (rs.next()) {
+                        String groupName = rs.getString(1);
+                        if (groupName != null) {
+                            Group group = findGroupInternal(dbConnection, groupName);
+                            if (group != null) {
+                                groups.add(group);
+                            }
+                        }
+                    }
+                }
+            } catch (SQLException e) {
+                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+            }
+        }
+
+        ArrayList<Role> roles = new ArrayList<>();
+        if (isRoleStoreDefined()) {
+            try (PreparedStatement stmt = dbConnection.prepareStatement(preparedRoles)) {
+                stmt.setString(1, userName);
+                try (ResultSet rs = stmt.executeQuery()) {
+                    while (rs.next()) {
+                        String roleName = rs.getString(1);
+                        if (roleName != null) {
+                            Role role = findRoleInternal(dbConnection, roleName);
+                            if (role != null) {
+                                roles.add(role);
+                            }
+                        }
+                    }
+                }
+            } catch (SQLException e) {
+                log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+            }
+        }
+
+        User user = new GenericUser<DataSourceUserDatabase>(this, userName, dbCredentials,
+                fullName, groups, roles);
+        return user;
+    }
+
+    @Override
+    public void modifiedGroup(Group group) {
+        String name = group.getName();
+        if (!createdGroups.containsKey(name) && !removedGroups.containsKey(name)) {
+            modifiedGroups.put(name, group);
+        }
+    }
+
+    @Override
+    public void modifiedRole(Role role) {
+        String name = role.getName();
+        if (!createdRoles.containsKey(name) && !removedRoles.containsKey(name)) {
+            modifiedRoles.put(name, role);
+        }
+    }
+
+    @Override
+    public void modifiedUser(User user) {
+        String name = user.getName();
+        if (!createdUsers.containsKey(name) && !removedUsers.containsKey(name)) {
+            modifiedUsers.put(name, user);
+        }
+    }
+
+    @Override
+    public void open() throws Exception {
+
+        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();
+
+        if (userGroupTable != null && userGroupTable.length() > 0) {
+            temp = new StringBuilder("SELECT ");
+            temp.append(groupNameCol);
+            temp.append(" FROM ");
+            temp.append(userGroupTable);
+            temp.append(" WHERE ");
+            temp.append(userNameCol);
+            temp.append(" = ?");
+            preparedGroups = temp.toString();
+        }
+
+        if (groupRoleTable != null && groupRoleTable.length() > 0) {
+            temp = new StringBuilder("SELECT ");
+            temp.append(groupNameCol);
+            temp.append(" FROM ");
+            temp.append(groupRoleTable);
+            temp.append(" WHERE ");
+            temp.append(groupNameCol);
+            temp.append(" = ?");
+            preparedGroupsR = temp.toString();
+        }
+
+        temp = new StringBuilder("SELECT ");
+        temp.append(userCredCol);
+        if (userFullNameCol != null) {
+            temp.append(",").append(userFullNameCol);
+        }
+        temp.append(" FROM ");
+        temp.append(userTable);
+        temp.append(" WHERE ");
+        temp.append(userNameCol);
+        temp.append(" = ?");
+        preparedUser = temp.toString();
+
+        temp = new StringBuilder("SELECT ");
+        temp.append(userNameCol);
+        temp.append(" FROM ");
+        temp.append(userTable);
+        preparedAllUsers = temp.toString();
+
+        if (groupTable != null && groupTable.length() > 0) {
+            temp = new StringBuilder("SELECT ");
+            temp.append(groupNameCol);
+            if (roleAndGroupDescriptionCol != null) {
+                temp.append(",").append(roleAndGroupDescriptionCol);
+            }
+            temp.append(" FROM ");
+            temp.append(groupTable);
+            temp.append(" WHERE ");
+            temp.append(groupNameCol);
+            temp.append(" = ?");
+            preparedGroup = temp.toString();
+
+            temp = new StringBuilder("SELECT ");
+            temp.append(groupNameCol);
+            temp.append(" FROM ");
+            temp.append(groupTable);
+            preparedAllGroups = temp.toString();
+        }
+
+        if (roleTable != null && roleTable.length() > 0) {
+            // Create the role PreparedStatement string
+            temp = new StringBuilder("SELECT ");
+            temp.append(roleNameCol);
+            if (roleAndGroupDescriptionCol != null) {
+                temp.append(",").append(roleAndGroupDescriptionCol);
+            }
+            temp.append(" FROM ");
+            temp.append(roleTable);
+            temp.append(" WHERE ");
+            temp.append(roleNameCol);
+            temp.append(" = ?");
+            preparedRole = temp.toString();
+
+            temp = new StringBuilder("SELECT ");
+            temp.append(roleNameCol);
+            temp.append(" FROM ");
+            temp.append(roleTable);
+            preparedAllRoles = temp.toString();
+        }
+
+    }
+
+    @Override
+    public void removeGroup(Group group) {
+        String name = group.getName();
+        createdGroups.remove(name);
+        modifiedGroups.remove(name);
+        removedGroups.put(name, group);
+    }
+
+    @Override
+    public void removeRole(Role role) {
+        String name = role.getName();
+        createdRoles.remove(name);
+        modifiedRoles.remove(name);
+        removedRoles.put(name, role);
+    }
+
+    @Override
+    public void removeUser(User user) {
+        String name = user.getName();
+        createdUsers.remove(name);
+        modifiedUsers.remove(name);
+        removedUsers.put(name, user);
+    }
+
+    @Override
+    public void save() throws Exception {
+        if (readonly) {
+            return;
+        }
+
+        Connection dbConnection = openConnection();
+        if (dbConnection == null) {
+            return;
+        }
+
+        try {
+            saveInternal(dbConnection);
+        } finally {
+            close(dbConnection);
+        }
+    }
+
+    protected void saveInternal(Connection dbConnection) {
+
+        StringBuilder temp = null;
+        StringBuilder tempRelation = null;
+        StringBuilder tempRelationDelete = null;
+
+        if (roleTable != null) {
+
+            // Created roles
+            if (!createdRoles.isEmpty()) {
+                temp = new StringBuilder("INSERT INTO ");
+                temp.append(roleTable);
+                temp.append('(').append(roleNameCol);
+                if (roleAndGroupDescriptionCol != null) {
+                    temp.append(',').append(roleAndGroupDescriptionCol);
+                }
+                temp.append(") VALUES (?");
+                if (roleAndGroupDescriptionCol != null) {
+                    temp.append(", ?");
+                }
+                temp.append(')');
+                for (Role role : createdRoles.values()) {
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                        stmt.setString(1, role.getRolename());
+                        if (roleAndGroupDescriptionCol != null) {
+                            stmt.setString(2, role.getDescription());
+                        }
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                createdRoles.clear();
+            }
+
+            // Modified roles
+            if (!modifiedRoles.isEmpty() && roleAndGroupDescriptionCol != null) {
+                temp = new StringBuilder("UPDATE ");
+                temp.append(roleTable);
+                temp.append(" SET ").append(roleAndGroupDescriptionCol);
+                temp.append(" = ? WHERE ").append(roleNameCol);
+                temp.append(" = ?");
+                for (Role role : modifiedRoles.values()) {
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                        stmt.setString(1, role.getDescription());
+                        stmt.setString(2, role.getRolename());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                modifiedRoles.clear();
+            }
+
+            // Removed roles
+            if (!removedRoles.isEmpty()) {
+                temp = new StringBuilder("DELETE FROM ");
+                temp.append(roleTable);
+                temp.append(" WHERE ").append(roleNameCol);
+                temp.append(" = ?");
+                if (groupRoleTable != null) {
+                    tempRelationDelete = new StringBuilder("DELETE FROM ");
+                    tempRelationDelete.append(groupRoleTable);
+                    tempRelationDelete.append(" WHERE ");
+                    tempRelationDelete.append(roleNameCol);
+                    tempRelationDelete.append(" = ?");
+                }
+                StringBuilder tempRelationDelete2 = new StringBuilder("DELETE FROM ");
+                tempRelationDelete2.append(userRoleTable);
+                tempRelationDelete2.append(" WHERE ");
+                tempRelationDelete2.append(roleNameCol);
+                tempRelationDelete2.append(" = ?");
+                for (Role role : removedRoles.values()) {
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete.toString())) {
+                        stmt.setString(1, role.getRolename());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete2.toString())) {
+                        stmt.setString(1, role.getRolename());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                        stmt.setString(1, role.getRolename());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                removedRoles.clear();
+            }
+
+        }
+
+        if (groupTable != null && groupRoleTable != null) {
+
+            tempRelation = new StringBuilder("INSERT INTO ");
+            tempRelation.append(groupRoleTable);
+            tempRelation.append('(').append(groupNameCol).append(", ");
+            tempRelation.append(roleNameCol);
+            tempRelation.append(") VALUES (?, ?)");
+            String groupRoleRelation = tempRelation.toString();
+            // Always drop and recreate all group <-> role relations
+            tempRelationDelete = new StringBuilder("DELETE FROM ");
+            tempRelationDelete.append(groupRoleTable);
+            tempRelationDelete.append(" WHERE ");
+            tempRelationDelete.append(groupNameCol);
+            tempRelationDelete.append(" = ?");
+            String groupRoleRelationDelete = tempRelationDelete.toString();
+
+            // Created groups
+            if (!createdGroups.isEmpty()) {
+                temp = new StringBuilder("INSERT INTO ");
+                temp.append(groupTable);
+                temp.append('(').append(groupNameCol);
+                if (roleAndGroupDescriptionCol != null) {
+                    temp.append(',').append(roleAndGroupDescriptionCol);
+                }
+                temp.append(") VALUES (?");
+                if (roleAndGroupDescriptionCol != null) {
+                    temp.append(", ?");
+                }
+                temp.append(')');
+                for (Group group : createdGroups.values()) {
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                        stmt.setString(1, group.getGroupname());
+                        if (roleAndGroupDescriptionCol != null) {
+                            stmt.setString(2, group.getDescription());
+                        }
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                    Iterator<Role> roles = group.getRoles();
+                    while (roles.hasNext()) {
+                        Role role = roles.next();
+                        try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelation)) {
+                            stmt.setString(1, group.getGroupname());
+                            stmt.setString(2, role.getRolename());
+                            stmt.executeUpdate();
+                        } catch (SQLException e) {
+                            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                        }
+                    }
+                }
+                createdGroups.clear();
+            }
+
+            // Modified groups
+            if (!modifiedGroups.isEmpty()) {
+                if (roleAndGroupDescriptionCol != null) {
+                    temp = new StringBuilder("UPDATE ");
+                    temp.append(groupTable);
+                    temp.append(" SET ").append(roleAndGroupDescriptionCol);
+                    temp.append(" = ? WHERE ").append(groupNameCol);
+                    temp.append(" = ?");
+                }
+                for (Group group : modifiedGroups.values()) {
+                    if (roleAndGroupDescriptionCol != null) {
+                        try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                            stmt.setString(1, group.getDescription());
+                            stmt.setString(2, group.getGroupname());
+                            stmt.executeUpdate();
+                        } catch (SQLException e) {
+                            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                        }
+                    }
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelationDelete)) {
+                        stmt.setString(1, group.getGroupname());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                    Iterator<Role> roles = group.getRoles();
+                    while (roles.hasNext()) {
+                        Role role = roles.next();
+                        try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelation)) {
+                            stmt.setString(1, group.getGroupname());
+                            stmt.setString(2, role.getRolename());
+                            stmt.executeUpdate();
+                        } catch (SQLException e) {
+                            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                        }
+                    }
+                }
+                modifiedGroups.clear();
+            }
+
+            // Removed groups
+            if (!removedGroups.isEmpty()) {
+                temp = new StringBuilder("DELETE FROM ");
+                temp.append(groupTable);
+                temp.append(" WHERE ").append(groupNameCol);
+                temp.append(" = ?");
+                StringBuilder tempRelationDelete2 = new StringBuilder("DELETE FROM ");
+                tempRelationDelete2.append(userGroupTable);
+                tempRelationDelete2.append(" WHERE ");
+                tempRelationDelete2.append(groupNameCol);
+                tempRelationDelete2.append(" = ?");
+                for (Group group : removedGroups.values()) {
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(groupRoleRelationDelete)) {
+                        stmt.setString(1, group.getGroupname());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete2.toString())) {
+                        stmt.setString(1, group.getGroupname());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                        stmt.setString(1, group.getGroupname());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                removedGroups.clear();
+            }
+
+        }
+
+        tempRelation = new StringBuilder("INSERT INTO ");
+        tempRelation.append(userRoleTable);
+        tempRelation.append('(').append(userNameCol).append(", ");
+        tempRelation.append(roleNameCol);
+        tempRelation.append(") VALUES (?, ?)");
+        String userRoleRelation = tempRelation.toString();
+        // Always drop and recreate all user <-> role relations
+        tempRelationDelete = new StringBuilder("DELETE FROM ");
+        tempRelationDelete.append(userRoleTable);
+        tempRelationDelete.append(" WHERE ");
+        tempRelationDelete.append(userNameCol);
+        tempRelationDelete.append(" = ?");
+        String userRoleRelationDelete = tempRelationDelete.toString();
+        String userGroupRelation = null;
+        String userGroupRelationDelete = null;
+        if (userGroupTable != null) {
+            tempRelation = new StringBuilder("INSERT INTO ");
+            tempRelation.append(userGroupTable);
+            tempRelation.append('(').append(userNameCol).append(", ");
+            tempRelation.append(groupNameCol);
+            tempRelation.append(") VALUES (?, ?)");
+            userGroupRelation = tempRelation.toString();
+            // Always drop and recreate all user <-> group relations
+            tempRelationDelete = new StringBuilder("DELETE FROM ");
+            tempRelationDelete.append(userGroupTable);
+            tempRelationDelete.append(" WHERE ");
+            tempRelationDelete.append(userNameCol);
+            tempRelationDelete.append(" = ?");
+            userGroupRelationDelete = tempRelationDelete.toString();
+        }
+
+        // Created users
+        if (!createdUsers.isEmpty()) {
+            temp = new StringBuilder("INSERT INTO ");
+            temp.append(userTable);
+            temp.append('(').append(userNameCol);
+            temp.append(", ").append(userCredCol);
+            if (userFullNameCol != null) {
+                temp.append(',').append(userFullNameCol);
+            }
+            temp.append(") VALUES (?, ?");
+            if (userFullNameCol != null) {
+                temp.append(", ?");
+            }
+            temp.append(')');
+            for (User user : createdUsers.values()) {
+                try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                    stmt.setString(1, user.getUsername());
+                    stmt.setString(2, user.getPassword());
+                    if (userFullNameCol != null) {
+                        stmt.setString(3, user.getFullName());
+                    }
+                    stmt.executeUpdate();
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                }
+                Iterator<Role> roles = user.getRoles();
+                while (roles.hasNext()) {
+                    Role role = roles.next();
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelation)) {
+                        stmt.setString(1, user.getUsername());
+                        stmt.setString(2, role.getRolename());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                if (userGroupRelation != null) {
+                    Iterator<Group> groups = user.getGroups();
+                    while (groups.hasNext()) {
+                        Group group = groups.next();
+                        try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelation)) {
+                            stmt.setString(1, user.getUsername());
+                            stmt.setString(2, group.getGroupname());
+                            stmt.executeUpdate();
+                        } catch (SQLException e) {
+                            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                        }
+                    }
+                }
+            }
+            createdUsers.clear();
+        }
+
+        // Modified users
+        if (!modifiedUsers.isEmpty()) {
+            temp = new StringBuilder("UPDATE ");
+            temp.append(userTable);
+            temp.append(" SET ").append(userCredCol);
+            temp.append(" = ?");
+            if (userFullNameCol != null) {
+                temp.append(", ").append(userFullNameCol).append(" = ?");
+            }
+            temp.append(" WHERE ").append(userNameCol);
+            temp.append(" = ?");
+            for (User user : modifiedUsers.values()) {
+                try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                    stmt.setString(1, user.getPassword());
+                    if (userFullNameCol != null) {
+                        stmt.setString(2, user.getFullName());
+                        stmt.setString(3, user.getUsername());
+                    } else {
+                        stmt.setString(2, user.getUsername());
+                    }
+                    stmt.executeUpdate();
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                }
+                try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelationDelete)) {
+                    stmt.setString(1, user.getUsername());
+                    stmt.executeUpdate();
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                }
+                if (userGroupRelationDelete != null) {
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelationDelete)) {
+                        stmt.setString(1, user.getUsername());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                Iterator<Role> roles = user.getRoles();
+                while (roles.hasNext()) {
+                    Role role = roles.next();
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelation)) {
+                        stmt.setString(1, user.getUsername());
+                        stmt.setString(2, role.getRolename());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                Iterator<Group> groups = user.getGroups();
+                while (groups.hasNext()) {
+                    Group group = groups.next();
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelation)) {
+                        stmt.setString(1, user.getUsername());
+                        stmt.setString(2, group.getGroupname());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+            }
+            modifiedGroups.clear();
+        }
+
+        // Removed users
+        if (!removedUsers.isEmpty()) {
+            temp = new StringBuilder("DELETE FROM ");
+            temp.append(userTable);
+            temp.append(" WHERE ").append(userNameCol);
+            temp.append(" = ?");
+            for (User user : removedUsers.values()) {
+                try (PreparedStatement stmt = dbConnection.prepareStatement(userRoleRelationDelete)) {
+                    stmt.setString(1, user.getUsername());
+                    stmt.executeUpdate();
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                }
+                if (userGroupRelationDelete != null) {
+                    try (PreparedStatement stmt = dbConnection.prepareStatement(userGroupRelationDelete)) {
+                        stmt.setString(1, user.getUsername());
+                        stmt.executeUpdate();
+                    } catch (SQLException e) {
+                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    }
+                }
+                try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
+                    stmt.setString(1, user.getUsername());
+                    stmt.executeUpdate();
+                } catch (SQLException e) {
+                    log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                }
+            }
+            removedUsers.clear();
+        }
+
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return connectionSuccess;
+    }
+
+    private boolean isGroupStoreDefined() {
+        return userGroupTable != null || groupNameCol != null;
+    }
+
+
+    private boolean isRoleStoreDefined() {
+        return userRoleTable != null || roleNameCol != null;
+    }
+
+
+    /**
+     * Open the specified database connection.
+     *
+     * @return Connection to the database
+     */
+    protected Connection openConnection() {
+        try {
+            Context context = namingContext;
+            DataSource dataSource = (DataSource) context.lookup(dataSourceName);
+            Connection connection = dataSource.getConnection();
+            connectionSuccess = true;
+            return connection;
+        } catch (Exception e) {
+            connectionSuccess = false;
+            // Log the problem for posterity
+            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+        }
+        return null;
+    }
+
+    /**
+     * 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) {
+            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+        }
+
+        // Close this database connection, and log any errors
+        try {
+            dbConnection.close();
+        } catch (SQLException e) {
+            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+        }
+
+    }
+
+
+}
diff --git a/java/org/apache/catalina/users/DataSourceUserDatabaseFactory.java b/java/org/apache/catalina/users/DataSourceUserDatabaseFactory.java
new file mode 100644
index 0000000..d288eff
--- /dev/null
+++ b/java/org/apache/catalina/users/DataSourceUserDatabaseFactory.java
@@ -0,0 +1,163 @@
+/*
+ * 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.users;
+
+
+import java.util.Hashtable;
+
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.RefAddr;
+import javax.naming.Reference;
+import javax.naming.spi.ObjectFactory;
+
+
+/**
+ * <p>JNDI object creation factory for <code>DataSourceUserDatabase</code>
+ * instances.  This makes it convenient to configure a user database
+ * in the global JNDI resources associated with this Catalina instance,
+ * and then link to that resource for web applications that administer
+ * the contents of the user database.</p>
+ *
+ * <p>The <code>DataSourceUserDatabase</code> instance is configured based
+ * on the following parameter values:</p>
+ * <ul>
+ * <li><strong>dataSourceName</strong> - JNDI name of the DataSource, which
+ * must be located in the same Context environment as the UserDatabase</li>
+ * </ul>
+ *
+ * @author Craig R. McClanahan
+ */
+public class DataSourceUserDatabaseFactory implements ObjectFactory {
+
+
+    // --------------------------------------------------------- Public Methods
+
+
+    /**
+     * <p>Create and return a new <code>DataSourceUserDatabase</code> instance
+     * that has been configured according to the properties of the
+     * specified <code>Reference</code>.  If you instance can be created,
+     * return <code>null</code> instead.</p>
+     *
+     * @param obj The possibly null object containing location or
+     *  reference information that can be used in creating an object
+     * @param name The name of this object relative to <code>nameCtx</code>
+     * @param nameCtx The context relative to which the <code>name</code>
+     *  parameter is specified, or <code>null</code> if <code>name</code>
+     *  is relative to the default initial context
+     * @param environment The possibly null environment that is used in
+     *  creating this object
+     */
+    @Override
+    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
+                                    Hashtable<?,?> environment)
+        throws Exception {
+
+        // We only know how to deal with <code>javax.naming.Reference</code>s
+        // that specify a class name of "org.apache.catalina.UserDatabase"
+        if ((obj == null) || !(obj instanceof Reference)) {
+            return null;
+        }
+        Reference ref = (Reference) obj;
+        if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) {
+            return null;
+        }
+
+        // Create and configure a MemoryUserDDataSourceUserDatabaseatabase instance based on the
+        // RefAddr values associated with this Reference
+        DataSourceUserDatabase database = new DataSourceUserDatabase(nameCtx, name.toString());
+        RefAddr ra = null;
+
+        ra = ref.get("dataSourceName");
+        if (ra != null) {
+            database.setDataSourceName(ra.getContent().toString());
+        }
+
+        ra = ref.get("readonly");
+        if (ra != null) {
+            database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
+        }
+
+        ra = ref.get("userTable");
+        if (ra != null) {
+            database.setUserTable(ra.getContent().toString());
+        }
+
+        ra = ref.get("groupTable");
+        if (ra != null) {
+            database.setGroupTable(ra.getContent().toString());
+        }
+
+        ra = ref.get("roleTable");
+        if (ra != null) {
+            database.setRoleTable(ra.getContent().toString());
+        }
+
+        ra = ref.get("userRoleTable");
+        if (ra != null) {
+            database.setUserRoleTable(ra.getContent().toString());
+        }
+
+        ra = ref.get("userGroupTable");
+        if (ra != null) {
+            database.setUserGroupTable(ra.getContent().toString());
+        }
+
+        ra = ref.get("groupRoleTable");
+        if (ra != null) {
+            database.setGroupRoleTable(ra.getContent().toString());
+        }
+
+        ra = ref.get("roleNameCol");
+        if (ra != null) {
+            database.setRoleNameCol(ra.getContent().toString());
+        }
+
+        ra = ref.get("roleAndGroupDescriptionCol");
+        if (ra != null) {
+            database.setRoleAndGroupDescriptionCol(ra.getContent().toString());
+        }
+
+        ra = ref.get("groupNameCol");
+        if (ra != null) {
+            database.setGroupNameCol(ra.getContent().toString());
+        }
+
+        ra = ref.get("userCredCol");
+        if (ra != null) {
+            database.setUserCredCol(ra.getContent().toString());
+        }
+
+        ra = ref.get("userFullNameCol");
+        if (ra != null) {
+            database.setUserFullNameCol(ra.getContent().toString());
+        }
+
+        ra = ref.get("userNameCol");
+        if (ra != null) {
+            database.setUserNameCol(ra.getContent().toString());
+        }
+
+        // Return the configured database instance
+        database.open();
+        return database;
+
+    }
+
+
+}
diff --git a/java/org/apache/catalina/users/GenericGroup.java b/java/org/apache/catalina/users/GenericGroup.java
index 986c9f7..2439788 100644
--- a/java/org/apache/catalina/users/GenericGroup.java
+++ b/java/org/apache/catalina/users/GenericGroup.java
@@ -169,4 +169,14 @@ public class GenericGroup<UD extends UserDatabase> extends AbstractGroup {
     }
 
 
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof GenericGroup) {
+            GenericGroup<?> group = (GenericGroup<?>) obj;
+            return group.database == database && groupname.equals(group.getGroupname());
+        }
+        return super.equals(obj);
+    }
+
+
 }
diff --git a/java/org/apache/catalina/users/GenericRole.java b/java/org/apache/catalina/users/GenericRole.java
index 2957280..4da7c4d 100644
--- a/java/org/apache/catalina/users/GenericRole.java
+++ b/java/org/apache/catalina/users/GenericRole.java
@@ -88,4 +88,14 @@ public class GenericRole<UD extends UserDatabase> extends AbstractRole {
     }
 
 
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof GenericRole) {
+            GenericRole<?> role = (GenericRole<?>) obj;
+            return role.database == database && rolename.equals(role.getRolename());
+        }
+        return super.equals(obj);
+    }
+
+
 }
diff --git a/java/org/apache/catalina/users/GenericUser.java b/java/org/apache/catalina/users/GenericUser.java
index 7d69360..8788cbd 100644
--- a/java/org/apache/catalina/users/GenericUser.java
+++ b/java/org/apache/catalina/users/GenericUser.java
@@ -244,4 +244,14 @@ public class GenericUser<UD extends UserDatabase> extends AbstractUser {
         super.setUsername(username);
     }
 
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof GenericUser) {
+            GenericUser<?> user = (GenericUser<?>) obj;
+            return user.database == database && username.equals(user.getUsername());
+        }
+        return super.equals(obj);
+    }
+
+
 }
diff --git a/java/org/apache/catalina/users/LocalStrings.properties b/java/org/apache/catalina/users/LocalStrings.properties
index 84d2754..4c946de 100644
--- a/java/org/apache/catalina/users/LocalStrings.properties
+++ b/java/org/apache/catalina/users/LocalStrings.properties
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+dataSourceUserDatabase.exception=Exception accessing the database
+
 memoryUserDatabase.fileClose=Failed to close [{0}]
 memoryUserDatabase.fileDelete=Failed to delete [{0}]
 memoryUserDatabase.fileNotFound=The specified user database [{0}] could not be found
diff --git a/java/org/apache/catalina/users/SparseUserDatabase.java b/java/org/apache/catalina/users/SparseUserDatabase.java
new file mode 100644
index 0000000..c0bc423
--- /dev/null
+++ b/java/org/apache/catalina/users/SparseUserDatabase.java
@@ -0,0 +1,29 @@
+/*
+ *  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.users;
+
+import org.apache.catalina.UserDatabase;
+
+public abstract class SparseUserDatabase implements UserDatabase {
+
+    @Override
+    public boolean isSparse() {
+        return true;
+    }
+
+
+}
diff --git a/java/org/apache/catalina/users/mbeans-descriptors.xml b/java/org/apache/catalina/users/mbeans-descriptors.xml
index ea7acfd..2175b40 100644
--- a/java/org/apache/catalina/users/mbeans-descriptors.xml
+++ b/java/org/apache/catalina/users/mbeans-descriptors.xml
@@ -153,5 +153,351 @@
                impact="ACTION"
            returnType="void">
     </operation>
+
+  </mbean>
+
+  <mbean         name="SparseUserDatabase"
+            className="org.apache.catalina.mbeans.SparseUserDatabaseMBean"
+          description="In-memory user and group database"
+               domain="Users"
+                group="UserDatabase"
+                 type="org.apache.catalina.users.SparseUserDatabase">
+
+    <attribute   name="groups"
+          description="MBean Names of all defined groups"
+                 type="[Ljava.lang.String;"
+            writeable="false"/>
+
+    <attribute   name="roles"
+          description="MBean Names of all defined roles"
+                 type="[Ljava.lang.String;"
+            writeable="false"/>
+
+    <attribute   name="users"
+          description="MBean Names of all defined users"
+                 type="[Ljava.lang.String;"
+            writeable="false"/>
+
+    <attribute   name="readonly"
+          description="No persistent save of the user database"
+                 type="boolean"
+            writeable="false"/>
+
+    <operation   name="createGroup"
+          description="Create new group and return MBean name"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="groupname"
+          description="Group name of the new group"
+                 type="java.lang.String"/>
+      <parameter name="description"
+          description="Description of the new group"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="createRole"
+          description="Create new role and return MBean name"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="rolename"
+          description="Role name of the new role"
+                 type="java.lang.String"/>
+      <parameter name="description"
+          description="Description of the new role"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="createUser"
+          description="Create new user and return MBean name"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="username"
+          description="User name of the new user"
+                 type="java.lang.String"/>
+      <parameter name="password"
+          description="Password of the new user"
+                 type="java.lang.String"/>
+      <parameter name="fullName"
+          description="Full name of the new user"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="findGroup"
+          description="Return MBean Name of the specified group (if any)"
+               impact="INFO"
+           returnType="java.lang.String">
+      <parameter name="groupname"
+          description="Group name of the requested group"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="findRole"
+          description="Return MBean Name of the specified role (if any)"
+               impact="INFO"
+           returnType="java.lang.String">
+      <parameter name="rolename"
+          description="Role name of the requested role"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="findUser"
+          description="Return MBean Name of the specified user (if any)"
+               impact="INFO"
+           returnType="java.lang.String">
+      <parameter name="username"
+          description="User name of the requested user"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeGroup"
+          description="Remove existing group (and all user memberships)"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="groupname"
+          description="Group name of the group to remove"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeRole"
+          description="Remove existing role"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="rolename"
+          description="Role name of the role to remove"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeUser"
+          description="Remove existing user (and all group memberships)"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="username"
+          description="User name of the user to remove"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="save"
+          description="Save current users and groups to persistent storage"
+               impact="ACTION"
+           returnType="void">
+    </operation>
+
+  </mbean>
+
+  <mbean         name="DataSourceUserDatabase"
+            className="org.apache.catalina.mbeans.DataSourceUserDatabaseMBean"
+          description="Lazy load user and group database"
+               domain="Catalina"
+                group="UserDatabase"
+                 type="org.apache.catalina.users.DataSourceUserDatabase">
+
+    <attribute   name="readonly"
+          description="No persistent save of the user database"
+                 type="boolean"
+            writeable="false"/>
+
+    <attribute   name="dataSourceName"
+          description="The name of the JNDI JDBC DataSource"
+                 type="java.lang.String"/>
+
+    <attribute   name="groups"
+          description="Names of all defined groups"
+                 type="[Ljava.lang.String;"
+            writeable="false"/>
+
+    <attribute   name="roles"
+          description="Names of all defined roles"
+                 type="[Ljava.lang.String;"
+            writeable="false"/>
+
+    <attribute   name="users"
+          description="Names of all defined users"
+                 type="[Ljava.lang.String;"
+            writeable="false"/>
+
+    <operation   name="createGroup"
+          description="Create new group and return name"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="groupname"
+          description="Group name of the new group"
+                 type="java.lang.String"/>
+      <parameter name="description"
+          description="Description of the new group"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="createRole"
+          description="Create new role and return name"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="rolename"
+          description="Role name of the new role"
+                 type="java.lang.String"/>
+      <parameter name="description"
+          description="Description of the new role"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="createUser"
+          description="Create new user and return name"
+               impact="ACTION"
+           returnType="java.lang.String">
+      <parameter name="username"
+          description="User name of the new user"
+                 type="java.lang.String"/>
+      <parameter name="password"
+          description="Password of the new user"
+                 type="java.lang.String"/>
+      <parameter name="fullName"
+          description="Full name of the new user"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeGroup"
+          description="Remove existing group (and all user memberships)"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="groupname"
+          description="Group name of the group to remove"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeRole"
+          description="Remove existing role"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="rolename"
+          description="Role name of the role to remove"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeUser"
+          description="Remove existing user (and all group memberships)"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="username"
+          description="User name of the user to remove"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="changeUserPassword"
+          description="Set new user credentials"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="username"
+          description="User name of the user"
+                 type="java.lang.String"/>
+      <parameter name="password"
+          description="New credentials"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="addUserRole"
+          description="Add role to user"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="username"
+          description="User name of the user"
+                 type="java.lang.String"/>
+      <parameter name="rolename"
+          description="Role name of the role"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeUserRole"
+          description="Remove role from user"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="username"
+          description="User name of the user"
+                 type="java.lang.String"/>
+      <parameter name="rolename"
+          description="Role name of the role"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="getUserRoles"
+          description="Get user roles"
+               impact="ACTION"
+           returnType="[Ljava.lang.String;">
+      <parameter name="username"
+          description="User name of the user"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="addUserGroup"
+          description="Add group to user"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="username"
+          description="User name of the user"
+                 type="java.lang.String"/>
+      <parameter name="groupname"
+          description="Group name of the group"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeUserGroup"
+          description="Remove group from user"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="username"
+          description="User name of the user"
+                 type="java.lang.String"/>
+      <parameter name="groupname"
+          description="Group name of the group"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="getUserGroups"
+          description="Get user groups"
+               impact="ACTION"
+           returnType="[Ljava.lang.String;">
+      <parameter name="username"
+          description="User name of the user"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="addGroupRole"
+          description="Add role to group"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="groupname"
+          description="Group name of the group"
+                 type="java.lang.String"/>
+      <parameter name="rolename"
+          description="Role name of the role"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="removeGroupRole"
+          description="Remove role from group"
+               impact="ACTION"
+           returnType="void">
+      <parameter name="groupname"
+          description="Group name of the group"
+                 type="java.lang.String"/>
+      <parameter name="rolename"
+          description="Role name of the role"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="getGroupRoles"
+          description="Get group roles"
+               impact="ACTION"
+           returnType="[Ljava.lang.String;">
+      <parameter name="groupname"
+          description="Group name of the group"
+                 type="java.lang.String"/>
+    </operation>
+
+    <operation   name="save"
+          description="Save current users and groups to persistent storage"
+               impact="ACTION"
+           returnType="void">
+    </operation>
+
   </mbean>
+
 </mbeans-descriptors>
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index d8eb83b..31e8be9 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -138,6 +138,16 @@
         intermediate concrete implementation classes and allowing to do
         partial database updates on <code>save</code>. (remm)
       </update>
+      <add>
+        Add a <code>UserDatabase</code> implementation as a superset of the
+        <code>DataSourceRealm</code> functionality. (remm)
+      </add>
+      <fix>
+        Make sure the dynamic Principal returned by
+        <code>UserDatabaseRealm</code> stays up to date with the database
+        contents, and add an option to have it be static, similar to the other
+        realms. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">
diff --git a/webapps/docs/config/realm.xml b/webapps/docs/config/realm.xml
index eaa8872..4f6569e 100644
--- a/webapps/docs/config/realm.xml
+++ b/webapps/docs/config/realm.xml
@@ -666,6 +666,14 @@
         that this realm will use for user, password and role information.</p>
       </attribute>
 
+      <attribute name="useStaticPrincipal" required="false">
+        <p>This allows using a static <code>Principal</code> instance
+        disconnected from the database if needed. This makes the bahavior of
+        authenticated prinicipals equivalent to that of the other realms.
+        If not specified, the default is <code>false</code>: use a
+        Principal connected to the UserDatabase.</p>
+      </attribute>
+
       <attribute name="transportGuaranteeRedirectStatus" required="false">
         <p>The HTTP status code to use when the container needs to issue an HTTP
            redirect to meet the requirements of a configured transport

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 08/16: Refactor to avoid NPE warnings in IDE

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 2996ab071f58ca689c5432801736093e7c327cbb
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Aug 31 18:02:36 2021 +0100

    Refactor to avoid NPE warnings in IDE
---
 java/org/apache/catalina/users/DataSourceUserDatabase.java | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/java/org/apache/catalina/users/DataSourceUserDatabase.java b/java/org/apache/catalina/users/DataSourceUserDatabase.java
index f90d1b0..dbe1a69 100644
--- a/java/org/apache/catalina/users/DataSourceUserDatabase.java
+++ b/java/org/apache/catalina/users/DataSourceUserDatabase.java
@@ -1092,11 +1092,13 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                 tempRelationDelete2.append(roleNameCol);
                 tempRelationDelete2.append(" = ?");
                 for (Role role : removedRoles.values()) {
-                    try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete.toString())) {
-                        stmt.setString(1, role.getRolename());
-                        stmt.executeUpdate();
-                    } catch (SQLException e) {
-                        log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                    if (tempRelationDelete != null) {
+                        try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete.toString())) {
+                            stmt.setString(1, role.getRolename());
+                            stmt.executeUpdate();
+                        } catch (SQLException e) {
+                            log.error(sm.getString("dataSourceUserDatabase.exception"), e);
+                        }
                     }
                     try (PreparedStatement stmt = dbConnection.prepareStatement(tempRelationDelete2.toString())) {
                         stmt.setString(1, role.getRolename());
@@ -1180,7 +1182,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                     temp.append(" = ?");
                 }
                 for (Group group : modifiedGroups.values()) {
-                    if (roleAndGroupDescriptionCol != null) {
+                    if (temp != null) {
                         try (PreparedStatement stmt = dbConnection.prepareStatement(temp.toString())) {
                             stmt.setString(1, group.getDescription());
                             stmt.setString(2, group.getGroupname());

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 09/16: Fix IDE warnings. Use <> where possible.

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit ce75358df10c6b56771d446c378b636f8dc1b646
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Aug 31 18:03:42 2021 +0100

    Fix IDE warnings. Use <> where possible.
---
 java/org/apache/catalina/users/DataSourceUserDatabase.java | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/java/org/apache/catalina/users/DataSourceUserDatabase.java b/java/org/apache/catalina/users/DataSourceUserDatabase.java
index dbe1a69..a5bff3a 100644
--- a/java/org/apache/catalina/users/DataSourceUserDatabase.java
+++ b/java/org/apache/catalina/users/DataSourceUserDatabase.java
@@ -559,7 +559,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
     public Group createGroup(String groupname, String description) {
         readLock.lock();
         try {
-            Group group = new GenericGroup<DataSourceUserDatabase>(this, groupname, description, null);
+            Group group = new GenericGroup<>(this, groupname, description, null);
             createdGroups.put(groupname, group);
             modifiedGroups.remove(groupname);
             removedGroups.remove(groupname);
@@ -573,7 +573,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
     public Role createRole(String rolename, String description) {
         readLock.lock();
         try {
-            Role role = new GenericRole<DataSourceUserDatabase>(this, rolename, description);
+            Role role = new GenericRole<>(this, rolename, description);
             createdRoles.put(rolename, role);
             modifiedRoles.remove(rolename);
             removedRoles.remove(rolename);
@@ -587,7 +587,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
     public User createUser(String username, String password, String fullName) {
         readLock.lock();
         try {
-            User user = new GenericUser<DataSourceUserDatabase>(this, username, password, fullName, null, null);
+            User user = new GenericUser<>(this, username, password, fullName, null, null);
             createdUsers.put(username, user);
             modifiedUsers.remove(username);
             removedUsers.remove(username);
@@ -657,7 +657,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                                 log.error(sm.getString("dataSourceUserDatabase.exception"), e);
                             }
                         }
-                        group = new GenericGroup<DataSourceUserDatabase>(this, groupName, description, groupRoles);
+                        group = new GenericGroup<>(this, groupName, description, groupRoles);
                     }
                 }
             }
@@ -707,7 +707,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
                 if (rs.next()) {
                     if (rs.getString(1) != null) {
                         String description = (roleAndGroupDescriptionCol != null) ? rs.getString(2) : null;
-                        role = new GenericRole<DataSourceUserDatabase>(this, roleName, description);
+                        role = new GenericRole<>(this, roleName, description);
                     }
                 }
             }
@@ -811,8 +811,7 @@ public class DataSourceUserDatabase extends SparseUserDatabase {
             }
         }
 
-        User user = new GenericUser<DataSourceUserDatabase>(this, userName, dbCredentials,
-                fullName, groups, roles);
+        User user = new GenericUser<>(this, userName, dbCredentials, fullName, groups, roles);
         return user;
     }
 

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 05/16: Fix compile

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit bbab0a61343d5a682c26dd652acac7ef0aa4def9
Author: remm <re...@apache.org>
AuthorDate: Wed Sep 1 13:41:29 2021 +0200

    Fix compile
---
 java/org/apache/catalina/realm/UserDatabaseRealm.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/java/org/apache/catalina/realm/UserDatabaseRealm.java b/java/org/apache/catalina/realm/UserDatabaseRealm.java
index ef072d6..8269a60 100644
--- a/java/org/apache/catalina/realm/UserDatabaseRealm.java
+++ b/java/org/apache/catalina/realm/UserDatabaseRealm.java
@@ -202,7 +202,7 @@ public class UserDatabaseRealm extends RealmBase {
             return null;
         } else {
             if (useStaticPrincipal) {
-                return new GenericPrincipal(username, Arrays.asList(getRoles(user))); 
+                return new GenericPrincipal(username, null, Arrays.asList(getRoles(user))); 
             } else {
                 return new UserDatabasePrincipal(user, database);
             }

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 16/16: Fix bad merge

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit a63ed576610f1fe46b689f8e672cfb0255e08428
Author: remm <re...@apache.org>
AuthorDate: Wed Sep 1 13:47:17 2021 +0200

    Fix bad merge
---
 webapps/docs/changelog.xml | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index aa5a111..abb2055 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -199,7 +199,6 @@
         Update to JSign version 4.0 to enable code signing without the need for
         the installation of additional client tools. (markt)
       </update>
-<<<<<<< HEAD
       <add>
         Update the internal fork of Apache Commons BCEL to 40d5eb4 (2021-09-01,
         6.6.0-SNAPSHOT). Code clean-up only. (markt)
@@ -208,12 +207,10 @@
         Update the internal fork of Apache Commons Codec to fd44e6b (2021-09-01,
         1.16-SNAPSHOT). Minor refactoring. (markt)
       </add>
-=======
       <update>
         Add Apache Derby 10.14.2.0 to the testsuite dependencies, for JDBC
         and DataSource testing. (remm)
       </update>
->>>>>>> 86177f0839 (Add Derby for the testsuite)
     </changelog>
   </subsection>
 </section>

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 14/16: Use Derby 10.14 for Java 8 compatibility

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit e1d0a7d778dede41c815327a7022c4b5dcfd3507
Author: remm <re...@apache.org>
AuthorDate: Wed Sep 1 13:00:49 2021 +0200

    Use Derby 10.14 for Java 8 compatibility
---
 build.properties.default   | 13 ++++---------
 build.xml                  | 10 ----------
 webapps/docs/changelog.xml |  2 +-
 3 files changed, 5 insertions(+), 20 deletions(-)

diff --git a/build.properties.default b/build.properties.default
index 80dd4fa..5d395ad 100644
--- a/build.properties.default
+++ b/build.properties.default
@@ -323,24 +323,19 @@ jsign.jar=${jsign.home}/jsign-${jsign.version}.jar
 jsign.loc=${base-maven.loc}/net/jsign/jsign/${jsign.version}/jsign-${jsign.version}.jar
 
 # ----- Derby, used by unit tests -----
-derby.version=10.15.2.0
+derby.version=10.14.2.0
 
-# checksums for Derby 10.15.2.0
+# checksums for Derby 10.14.2.0
 derby.checksum.enabled=true
 derby.checksum.algorithm=MD5|SHA-1
-derby.checksum.value=abff01351b19bc62a188bac08a8bb58b|b64da6681994f33ba5783ffae55cdb44885b9e70
-derby-shared.checksum.enabled=true
-derby-shared.checksum.algorithm=MD5|SHA-1
-derby-shared.checksum.value=2cb9ab8b9cfb06c2da5a1d3825d04344|ff2dfb3e2a92d593cf111baad242d156947abbc1
+derby.checksum.value=3ddcc1d435344d39d0122dbc2f39a746|7efad40ef52fbb1f08142f07a83b42d29e47d8ce
 derby-tools.checksum.enabled=true
 derby-tools.checksum.algorithm=MD5|SHA-1
-derby-tools.checksum.value=d41578eeb336b0e479be8f30bfd9ab9b|d63722381e0e893d797e4d531e219e2917898364
+derby-tools.checksum.value=3189a1d586f98f0d203fb5f3e5d88fbe|338d5a54b4089c80414fe0ecb3899d521da69b26
 
 derby.home=${base.path}/derby-${derby.version}
 derby.jar=${derby.home}/derby-${derby.version}.jar
 derby.loc=${base-maven.loc}/org/apache/derby/derby/${derby.version}/derby-${derby.version}.jar
-derby-shared.jar=${derby.home}/derby-shared-${derby.version}.jar
-derby-shared.loc=${base-maven.loc}/org/apache/derby/derbyshared/${derby.version}/derbyshared-${derby.version}.jar
 derby-tools.jar=${derby.home}/derby-tools-${derby.version}.jar
 derby-tools.loc=${base-maven.loc}/org/apache/derby/derbytools/${derby.version}/derbytools-${derby.version}.jar
 
diff --git a/build.xml b/build.xml
index 34301df..4566582 100644
--- a/build.xml
+++ b/build.xml
@@ -261,7 +261,6 @@
     <pathelement location="${objenesis.jar}"/>
     <pathelement location="${unboundid.jar}"/>
     <pathelement location="${derby.jar}"/>
-    <pathelement location="${derby-shared.jar}"/>
     <pathelement location="${derby-tools.jar}"/>
     <path refid="compile.classpath" />
     <path refid="tomcat.classpath" />
@@ -3260,15 +3259,6 @@ skip.installer property in build.properties" />
     </antcall>
 
     <antcall target="downloadfile">
-      <param name="sourcefile" value="${derby-shared.loc}"/>
-      <param name="destfile" value="${derby-shared.jar}"/>
-      <param name="destdir" value="${derby.home}"/>
-      <param name="checksum.enabled" value="${derby-shared.checksum.enabled}"/>
-      <param name="checksum.algorithm" value="${derby-shared.checksum.algorithm}"/>
-      <param name="checksum.value" value="${derby-shared.checksum.value}"/>
-    </antcall>
-
-    <antcall target="downloadfile">
       <param name="sourcefile" value="${derby-tools.loc}"/>
       <param name="destfile" value="${derby-tools.jar}"/>
       <param name="destdir" value="${derby.home}"/>
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 4b2b413..aa5a111 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -210,7 +210,7 @@
       </add>
 =======
       <update>
-        Add Apache Derby 10.15.2.0 to the testsuite dependencies, for JDBC
+        Add Apache Derby 10.14.2.0 to the testsuite dependencies, for JDBC
         and DataSource testing. (remm)
       </update>
 >>>>>>> 86177f0839 (Add Derby for the testsuite)

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


[tomcat] 10/16: Add hashCode implementations that are aligned with existing equals()

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 5335d2562483ec4ffc20503723c952e801e204fb
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Aug 31 18:16:03 2021 +0100

    Add hashCode implementations that are aligned with existing equals()
---
 java/org/apache/catalina/users/GenericGroup.java | 8 ++++++++
 java/org/apache/catalina/users/GenericRole.java  | 8 ++++++++
 java/org/apache/catalina/users/GenericUser.java  | 8 ++++++++
 3 files changed, 24 insertions(+)

diff --git a/java/org/apache/catalina/users/GenericGroup.java b/java/org/apache/catalina/users/GenericGroup.java
index 2439788..153f5e5 100644
--- a/java/org/apache/catalina/users/GenericGroup.java
+++ b/java/org/apache/catalina/users/GenericGroup.java
@@ -179,4 +179,12 @@ public class GenericGroup<UD extends UserDatabase> extends AbstractGroup {
     }
 
 
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((database == null) ? 0 : database.hashCode());
+        result = prime * result + ((groupname == null) ? 0 : groupname.hashCode());
+        return result;
+    }
 }
diff --git a/java/org/apache/catalina/users/GenericRole.java b/java/org/apache/catalina/users/GenericRole.java
index 4da7c4d..2103714 100644
--- a/java/org/apache/catalina/users/GenericRole.java
+++ b/java/org/apache/catalina/users/GenericRole.java
@@ -98,4 +98,12 @@ public class GenericRole<UD extends UserDatabase> extends AbstractRole {
     }
 
 
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((database == null) ? 0 : database.hashCode());
+        result = prime * result + ((rolename == null) ? 0 : rolename.hashCode());
+        return result;
+    }
 }
diff --git a/java/org/apache/catalina/users/GenericUser.java b/java/org/apache/catalina/users/GenericUser.java
index 8788cbd..a020acd 100644
--- a/java/org/apache/catalina/users/GenericUser.java
+++ b/java/org/apache/catalina/users/GenericUser.java
@@ -254,4 +254,12 @@ public class GenericUser<UD extends UserDatabase> extends AbstractUser {
     }
 
 
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((database == null) ? 0 : database.hashCode());
+        result = prime * result + ((username == null) ? 0 : username.hashCode());
+        return result;
+    }
 }

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org