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

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


The following commit(s) were added to refs/heads/main by this push:
     new 299d061  Add a UserDatabase implementation based on DataSourceRealm
299d061 is described below

commit 299d061080774d3a898335d11bc981b23caa859d
Author: remm <r...@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 0063c37..7c4343d 100644
--- a/java/org/apache/catalina/mbeans/LocalStrings.properties
+++ b/java/org/apache/catalina/mbeans/LocalStrings.properties
@@ -60,3 +60,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 477be7f..d7806df 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());
-            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 65eb5f7..aeb311b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -138,6 +138,16 @@
         scheme specific check can be performed. Based on pull request
         <pr>444</pr> by Robert Rodewald. (markt)
       </scode>
+      <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 05e50bf..32c28b0 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-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to