GUACAMOLE-820: Merge match IP address filters anywhere in object properties.

diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
index 646bf20..1bb2c68 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
@@ -139,6 +139,7 @@
         
         // Initialize the UserContext with the user account and return it.
         context.init(user.getCurrentUser());
+        context.recordUserLogin();
         return context;
 
     }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java
index 1e52571..2a4cd9e 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java
@@ -82,22 +82,24 @@
         // Retrieve permissions only if allowed
         if (canReadPermissions(user, targetEntity)) {
 
-            // Only administrators may access active connections
-            boolean isAdmin = targetEntity.isAdministrator();
+            // Privileged accounts (such as administrators or UserContexts
+            // returned by getPrivileged()) may always access active connections
+            boolean isPrivileged = targetEntity.isPrivileged();
 
             // Get all active connections
             Collection<ActiveConnectionRecord> records = tunnelService.getActiveConnections(user);
 
             // We have READ, and possibly DELETE, on all active connections
-            Set<ObjectPermission> permissions = new HashSet<ObjectPermission>();
+            Set<ObjectPermission> permissions = new HashSet<>();
             for (ActiveConnectionRecord record : records) {
 
                 // Add implicit READ
                 String identifier = record.getUUID().toString();
                 permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier));
 
-                // If we're an admin, or the connection is ours, then we can DELETE
-                if (isAdmin || targetEntity.isUser(record.getUsername()))
+                // If the target user is privileged, or the connection belongs
+                // to the target user, then they can DELETE
+                if (isPrivileged || targetEntity.isUser(record.getUsername()))
                     permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier));
 
             }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
index c21e9c3..046cee1 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
@@ -81,7 +81,7 @@
             Collection<String> identifiers) throws GuacamoleException {
 
         String username = user.getIdentifier();
-        boolean isAdmin = user.getUser().isAdministrator();
+        boolean isPrivileged = user.isPrivileged();
         Set<String> identifierSet = new HashSet<String>(identifiers);
 
         // Retrieve all visible connections (permissions enforced by tunnel service)
@@ -95,7 +95,7 @@
             // be able to connect to (join) the active connection if they are
             // the user that started the connection OR the user is an admin
             boolean hasPrivilegedAccess =
-                    isAdmin || username.equals(record.getUsername());
+                    isPrivileged || username.equals(record.getUsername());
 
             // Add connection if within requested identifiers
             if (identifierSet.contains(record.getUUID().toString())) {
@@ -211,7 +211,7 @@
         
         ObjectPermissionSet permissionSet = getPermissionSet(user);
         
-        return user.getUser().isAdministrator() 
+        return user.isPrivileged()
                 || permissionSet.hasPermission(type, identifier);
         
     }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledChildDirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledChildDirectoryObjectService.java
index f517e27..7690313 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledChildDirectoryObjectService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledChildDirectoryObjectService.java
@@ -148,8 +148,8 @@
     protected boolean canUpdateModifiedParents(ModeledAuthenticatedUser user,
             String identifier, ModelType model) throws GuacamoleException {
 
-        // If user is an administrator, no need to check
-        if (user.getUser().isAdministrator())
+        // If user is privileged, no need to check
+        if (user.isPrivileged())
             return true;
         
         // Verify that we have permission to modify any modified parents
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java
index edbb67e..f8d0e8a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java
@@ -171,7 +171,7 @@
         ObjectPermissionSet permissionSet = getEffectivePermissionSet(user);
         
         // Return whether permission is granted
-        return user.getUser().isAdministrator()
+        return user.isPrivileged()
             || permissionSet.hasPermission(type, identifier);
 
     }
@@ -248,7 +248,7 @@
             ExternalType object, ModelType model) throws GuacamoleException {
 
         // Verify permission to create objects
-        if (!user.getUser().isAdministrator() && !hasCreatePermission(user))
+        if (!user.isPrivileged() && !hasCreatePermission(user))
             throw new GuacamoleSecurityException("Permission denied.");
 
     }
@@ -395,8 +395,8 @@
 
         Collection<ModelType> objects;
 
-        // Bypass permission checks if the user is a system admin
-        if (user.getUser().isAdministrator())
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
             objects = getObjectMapper().select(identifiers);
 
         // Otherwise only return explicitly readable identifiers
@@ -507,8 +507,8 @@
     public Set<String> getIdentifiers(ModeledAuthenticatedUser user)
         throws GuacamoleException {
 
-        // Bypass permission checks if the user is a system admin
-        if (user.getUser().isAdministrator())
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
             return getObjectMapper().selectIdentifiers();
 
         // Otherwise only return explicitly readable identifiers
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledPermissions.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledPermissions.java
index 965062c..be5ac15 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledPermissions.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledPermissions.java
@@ -132,18 +132,20 @@
     }
 
     /**
-     * Returns whether this entity is a system administrator, and thus is not
-     * restricted by permissions, taking into account permission inheritance
-     * via user groups.
+     * Returns whether this entity is effectively unrestricted by permissions,
+     * such as a system administrator or an internal user operating via a
+     * privileged UserContext. Permission inheritance via user groups is taken
+     * into account.
      *
      * @return
-     *    true if this entity is a system administrator, false otherwise.
+     *     true if this entity should be unrestricted by permissions, false
+     *     otherwise.
      *
      * @throws GuacamoleException
-     *    If an error occurs while determining the entity's system administrator
-     *    status.
+     *     If an error occurs while determining whether permission restrictions
+     *     apply to the entity.
      */
-    public boolean isAdministrator() throws GuacamoleException {
+    public boolean isPrivileged() throws GuacamoleException {
         SystemPermissionSet systemPermissionSet = getEffective().getSystemPermissions();
         return systemPermissionSet.hasPermission(SystemPermission.Type.ADMINISTER);
     }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RelatedObjectSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RelatedObjectSet.java
index f7b75ef..810e9a5 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RelatedObjectSet.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RelatedObjectSet.java
@@ -140,8 +140,9 @@
     private boolean canAlterRelation(Collection<String> identifiers)
             throws GuacamoleException {
 
-        // System administrators may alter any relations
-        if (getCurrentUser().getUser().isAdministrator())
+        // Privileged users (such as system administrators) may alter any
+        // relations
+        if (getCurrentUser().isPrivileged())
             return true;
 
         // Non-admin users require UPDATE permission on the parent object ...
@@ -162,9 +163,9 @@
     @Override
     public Set<String> getObjects() throws GuacamoleException {
 
-        // Bypass permission checks if the user is a system admin
+        // Bypass permission checks if the user is a privileged
         ModeledAuthenticatedUser user = getCurrentUser();
-        if (user.getUser().isAdministrator())
+        if (user.isPrivileged())
             return getObjectRelationMapper().selectChildIdentifiers(parent.getModel());
 
         // Otherwise only return explicitly readable identifiers
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
index e2f3c15..926df32 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
@@ -297,8 +297,8 @@
             String identifier)
             throws GuacamoleException {
 
-        // Bypass permission checks if the user is a system admin
-        if (user.getUser().isAdministrator())
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
             return connectionMapper.selectIdentifiersWithin(identifier);
 
         // Otherwise only return explicitly readable identifiers
@@ -470,8 +470,8 @@
 
         List<ConnectionRecordModel> searchResults;
 
-        // Bypass permission checks if the user is a system admin
-        if (user.getUser().isAdministrator())
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
             searchResults = connectionRecordMapper.search(requiredContents,
                     sortPredicates, limit);
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java
index 3e9ec72..dbf7793 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java
@@ -218,8 +218,8 @@
             String identifier)
             throws GuacamoleException {
 
-        // Bypass permission checks if the user is a system admin
-        if (user.getUser().isAdministrator())
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
             return connectionGroupMapper.selectIdentifiersWithin(identifier);
 
         // Otherwise only return explicitly readable identifiers
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java
index eea570f..eb320bd 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java
@@ -104,8 +104,8 @@
         if (targetEntity.isUser(user.getUser().getIdentifier()))
             return true;
         
-        // A system adminstrator can do anything
-        if (user.getUser().isAdministrator())
+        // Privileged users (such as system administrators) may do anything
+        if (user.isPrivileged())
             return true;
 
         // Can read permissions on target entity if explicit READ is granted
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/ModeledObjectPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/ModeledObjectPermissionService.java
index 8c4be58..820873a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/ModeledObjectPermissionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/ModeledObjectPermissionService.java
@@ -95,8 +95,8 @@
             Collection<ObjectPermission> permissions)
             throws GuacamoleException {
 
-        // A system adminstrator can do anything
-        if (user.getUser().isAdministrator())
+        // Privileged users (such as system administrators) may do anything
+        if (user.isPrivileged())
             return true;
         
         // Verify user has update permission on the target entity
@@ -187,8 +187,8 @@
         if (identifiers.isEmpty())
             return identifiers;
         
-        // If user is an admin, everything is accessible
-        if (user.getUser().isAdministrator())
+        // Privileged users (such as system administrators) may access everything
+        if (user.isPrivileged())
             return identifiers;
 
         // Otherwise, return explicitly-retrievable identifiers only if allowed
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/SystemPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/SystemPermissionService.java
index 5e5a43b..4b8269f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/SystemPermissionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/SystemPermissionService.java
@@ -94,8 +94,9 @@
             ModeledPermissions<? extends EntityModel> targetEntity,
             Collection<SystemPermission> permissions) throws GuacamoleException {
 
-        // Only an admin can create system permissions
-        if (user.getUser().isAdministrator()) {
+        // Only privileged users (such as system administrators) can create
+        // system permissions
+        if (user.isPrivileged()) {
             Collection<SystemPermissionModel> models = getModelInstances(targetEntity, permissions);
             systemPermissionMapper.insert(models);
             return;
@@ -111,8 +112,9 @@
             ModeledPermissions<? extends EntityModel> targetEntity,
             Collection<SystemPermission> permissions) throws GuacamoleException {
 
-        // Only an admin can delete system permissions
-        if (user.getUser().isAdministrator()) {
+        // Only privileged users (such as system administrators) can delete
+        // system permissions
+        if (user.isPrivileged()) {
 
             // Do not allow users to remove their own admin powers
             if (user.getUser().getIdentifier().equals(targetEntity.getIdentifier()))
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
index abecf32..383ef3a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
@@ -628,8 +628,9 @@
         if (records.isEmpty())
             return Collections.<ActiveConnectionRecord>emptyList();
 
-        // A system administrator can view all connections; no need to filter
-        if (user.getUser().isAdministrator())
+        // Privileged users (such as system administrators) can view all
+        // connections; no need to filter
+        if (user.isPrivileged())
             return records;
 
         // Build set of all connection identifiers associated with active tunnels
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java
index 5778ad0..7ede92c 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java
@@ -23,6 +23,7 @@
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
@@ -176,4 +177,22 @@
                 super.getEffectiveUserGroups());
     }
 
+    /**
+     * Returns whether this user is effectively unrestricted by permissions,
+     * such as a system administrator or an internal user operating via a
+     * privileged UserContext. Permission inheritance via user groups is taken
+     * into account.
+     *
+     * @return
+     *     true if this user should be unrestricted by permissions, false
+     *     otherwise.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs while determining whether permission restrictions
+     *     apply to the user.
+     */
+    public boolean isPrivileged() throws GuacamoleException {
+        return getUser().isPrivileged();
+    }
+
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java
index e98a25a..fe60a5f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java
@@ -48,6 +48,7 @@
 import org.apache.guacamole.net.auth.Directory;
 import org.apache.guacamole.net.auth.SharingProfile;
 import org.apache.guacamole.net.auth.User;
+import org.apache.guacamole.net.auth.UserContext;
 import org.apache.guacamole.net.auth.UserGroup;
 
 /**
@@ -118,13 +119,22 @@
     private Provider<UserRecordSet> userRecordSetProvider;
 
     /**
+     * Provider for retrieving UserContext instances.
+     */
+    @Inject
+    private Provider<ModeledUserContext> userContextProvider;
+
+    /**
      * Mapper for user login records.
      */
     @Inject
     private UserRecordMapper userRecordMapper;
 
     /**
-     * The activity record associated with this user's Guacamole session.
+     * The activity record associated with this user's Guacamole session. If
+     * this user's session will not have an associated activity record, such as
+     * a temporary privileged session created via getPrivileged(), this will be
+     * null.
      */
     private ActivityRecordModel userRecord;
 
@@ -141,15 +151,40 @@
         sharingProfileDirectory.init(currentUser);
         activeConnectionDirectory.init(currentUser);
 
+    }
+
+    /**
+     * Records that the user associated with this UserContext has logged in,
+     * creating a partial activity record. The resulting activity record will
+     * contain a start date only, with the end date being automatically
+     * populated when this UserContext is invalidated. If this function is
+     * invoked more than once for the same UserContext, only the first
+     * invocation has any effect. If this function is never invoked, no
+     * activity record will be recorded, including when this UserContext is
+     * invalidated.
+     */
+    public void recordUserLogin() {
+
+        // Do nothing if invoked multiple times
+        if (userRecord != null)
+            return;
+
         // Create login record for user
         userRecord = new ActivityRecordModel();
-        userRecord.setUsername(currentUser.getIdentifier());
+        userRecord.setUsername(getCurrentUser().getIdentifier());
         userRecord.setStartDate(new Date());
-        userRecord.setRemoteHost(currentUser.getCredentials().getRemoteAddress());
+        userRecord.setRemoteHost(getCurrentUser().getCredentials().getRemoteAddress());
 
         // Insert record representing login
         userRecordMapper.insert(userRecord);
+        
+    }
 
+    @Override
+    public UserContext getPrivileged() {
+        ModeledUserContext context = userContextProvider.get();
+        context.init(new PrivilegedModeledAuthenticatedUser(getCurrentUser()));
+        return context;
     }
 
     @Override
@@ -253,9 +288,11 @@
     @Override
     public void invalidate() {
 
-        // Record logout time
-        userRecord.setEndDate(new Date());
-        userRecordMapper.update(userRecord);
+        // Record logout time only if login time was recorded
+        if (userRecord != null) {
+            userRecord.setEndDate(new Date());
+            userRecordMapper.update(userRecord);
+        }
 
     }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PrivilegedModeledAuthenticatedUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PrivilegedModeledAuthenticatedUser.java
new file mode 100644
index 0000000..82fcf08
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/PrivilegedModeledAuthenticatedUser.java
@@ -0,0 +1,50 @@
+/*
+ * 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.guacamole.auth.jdbc.user;
+
+import org.apache.guacamole.GuacamoleException;
+
+/**
+ * A ModeledAuthenticatedUser which is always privileged, returning true for
+ * every call to isPrivileged().
+ */
+public class PrivilegedModeledAuthenticatedUser extends ModeledAuthenticatedUser {
+
+    /**
+     * Creates a new PrivilegedModeledAuthenticatedUser which shares the same
+     * user identity as the given ModeledAuthenticatedUser. Regardless of the
+     * privileges explicitly granted to the given user, the resulting
+     * PrivilegedModeledAuthenticatedUser will always assert that it is
+     * privileged.
+     *
+     * @param authenticatedUser
+     *     The ModeledAuthenticatedUser that declares the identity of the user
+     *     in question.
+     */
+    public PrivilegedModeledAuthenticatedUser(ModeledAuthenticatedUser authenticatedUser){
+        super(authenticatedUser, authenticatedUser.getModelAuthenticationProvider(), authenticatedUser.getUser());
+    }
+
+    @Override
+    public boolean isPrivileged() throws GuacamoleException {
+        return true;
+    }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
index a68f082..0aecd10 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
@@ -278,8 +278,8 @@
         // Verify new password does not violate defined policies (if specified)
         if (object.getPassword() != null) {
 
-            // Enforce password age only for non-adminstrators
-            if (!user.getUser().isAdministrator())
+            // Enforce password age only for non-privileged users
+            if (!user.isPrivileged())
                 passwordPolicyService.verifyPasswordAge(object);
 
             // Always verify password complexity
@@ -626,8 +626,8 @@
 
         List<ActivityRecordModel> searchResults;
 
-        // Bypass permission checks if the user is a system admin
-        if (user.getUser().isAdministrator())
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
             searchResults = userRecordMapper.search(requiredContents,
                     sortPredicates, limit);
 
diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java
index a73d08f..e0cd84b 100644
--- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java
+++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java
@@ -181,12 +181,13 @@
 
         // Update user object
         try {
-            context.getUserDirectory().update(self);
+            context.getPrivileged().getUserDirectory().update(self);
         }
         catch (GuacamoleSecurityException e) {
             logger.info("User \"{}\" cannot store their TOTP key as they "
-                    + "lack permission to update their own account. TOTP "
-                    + "will be disabled for this user.",
+                    + "lack permission to update their own account and the "
+                    + "TOTP extension was unable to obtain privileged access. "
+                    + "TOTP will be disabled for this user.",
                     self.getIdentifier());
             logger.debug("Permission denied to set TOTP key of user "
                     + "account.", e);
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUserContext.java
index eb31f7e..c4dbf10 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUserContext.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUserContext.java
@@ -254,4 +254,16 @@
     public void invalidate() {
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This implementation simply returns <code>this</code>. Implementations
+     * that wish to provide additional privileges to extensions requesting
+     * privileged access should override this function.
+     */
+    @Override
+    public UserContext getPrivileged() {
+        return this;
+    }
+
 }
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java
index 9db6adb..85e0259 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java
@@ -152,4 +152,9 @@
         userContext.invalidate();
     }
 
+    @Override
+    public UserContext getPrivileged() {
+        return userContext.getPrivileged();
+    }
+
 }
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java
index ea7c8c4..ccdcaae 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java
@@ -262,4 +262,29 @@
      */
     void invalidate();
 
+    /**
+     * Returns a user context which provides privileged access. Unlike the
+     * original user context, which is required to enforce its own permissions
+     * and act only within the rights of the associated user, the user context
+     * returned by this function MAY ignore the restrictions that otherwise
+     * limit the current user's access.
+     *
+     * <p>This function is intended to allow extensions which decorate other
+     * extensions to act independently of the restrictions that affect the
+     * current user. This function will only be invoked by extensions and
+     * WILL NOT be invoked directly by the web application. Implementations of
+     * this function MAY still enforce access restrictions, particularly if
+     * they do not want to grant full, unrestricted access to other extensions.
+     *
+     * <p>A default implementation which simply returns <code>this</code> is
+     * provided for compatibility with Apache Guacamole 1.1.0 and older.
+     *
+     * @return
+     *     A user context instance which MAY ignore some or all restrictions
+     *     which otherwise limit the current user's access.
+     */
+    default UserContext getPrivileged() {
+        return this;
+    }
+
 }
diff --git a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html
index 4c9bb42..40145c8 100644
--- a/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html
+++ b/guacamole/src/main/webapp/app/groupList/templates/guacGroupList.html
@@ -3,6 +3,7 @@
     <script type="text/ng-template" id="nestedItem.html">
         <div class="{{item.type}}" ng-if="isVisible(item.type)"
             ng-class="{
+                balancer   : item.balancing,
                 expanded   : item.expanded,
                 expandable : item.expandable,
                 empty      : !item.children.length
diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js
index 29bf91b..2255cb4 100644
--- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js
+++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js
@@ -145,7 +145,7 @@
                 });
 
             // If the item is a connection group, generate a connection group identifier
-            if (this.type === GroupListItem.Type.CONNECTION_GROUP)
+            if (this.type === GroupListItem.Type.CONNECTION_GROUP && this.balancing)
                 return ClientIdentifier.toString({
                     dataSource : this.dataSource,
                     type       : ClientIdentifier.Types.CONNECTION_GROUP,
@@ -158,6 +158,27 @@
         };
 
         /**
+         * Returns the relative URL of the client page that connects to the
+         * connection or connection group represented by this GroupListItem.
+         *
+         * @returns {String}
+         *     The relative URL of the client page that connects to the
+         *     connection or connection group represented by this GroupListItem,
+         *     or null if this GroupListItem cannot be connected to.
+         */
+        this.getClientURL = template.getClientURL || function getClientURL() {
+
+            // There is a client page for this item only if it has an
+            // associated client identifier
+            var identifier = this.getClientIdentifier();
+            if (identifier)
+                return '#/client/' + encodeURIComponent(identifier);
+
+            return null;
+
+        };
+
+        /**
          * The connection, connection group, or sharing profile whose data is
          * exposed within this GroupListItem. If the type of this GroupListItem
          * is not one of the types defined by GroupListItem.Type, then this
diff --git a/guacamole/src/main/webapp/app/home/styles/home.css b/guacamole/src/main/webapp/app/home/styles/home.css
index fb01cac..33b867a 100644
--- a/guacamole/src/main/webapp/app/home/styles/home.css
+++ b/guacamole/src/main/webapp/app/home/styles/home.css
@@ -51,6 +51,27 @@
     overflow: hidden;
 }
 
-a.home-connection {
+a.home-connection, .empty.balancer a.home-connection-group {
     display: block;
 }
+
+/* Show only expand/collapse icon for connection groups on home screen ... */
+
+.all-connections .connection-group > .caption .icon {
+    display: none;
+}
+
+.all-connections .connection-group > .caption .icon.expand {
+    display: inline-block;
+}
+
+/* ... except for empty balancing groups, which should be rendered as if they
+ * are connections. */
+
+.all-connections .connection-group.empty.balancer > .caption .icon {
+    display: inline-block;
+}
+
+.all-connections .connection-group.empty.balancer > .caption .icon.expand {
+    display: none;
+}
diff --git a/guacamole/src/main/webapp/app/home/templates/connection.html b/guacamole/src/main/webapp/app/home/templates/connection.html
index b428412..85a7f57 100644
--- a/guacamole/src/main/webapp/app/home/templates/connection.html
+++ b/guacamole/src/main/webapp/app/home/templates/connection.html
@@ -1,5 +1,5 @@
 <a class="home-connection"
-   ng-href="#/client/{{ item.getClientIdentifier() }}"
+   ng-href="{{ item.getClientURL() }}"
    ng-class="{active: item.getActiveConnections()}">
 
     <!-- Connection icon -->
diff --git a/guacamole/src/main/webapp/app/home/templates/connectionGroup.html b/guacamole/src/main/webapp/app/home/templates/connectionGroup.html
index 909aacf..ba0a204 100644
--- a/guacamole/src/main/webapp/app/home/templates/connectionGroup.html
+++ b/guacamole/src/main/webapp/app/home/templates/connectionGroup.html
@@ -1,4 +1,10 @@
-<span class="home-connection-group name">
-    <a ng-show="item.balancing" ng-href="#/client/{{ item.getClientIdentifier() }}">{{item.name}}</a>
-    <span ng-show="!item.balancing">{{item.name}}</span>
-</span>
+<a class="home-connection-group"
+   ng-href="{{ item.getClientURL() }}">
+
+    <!-- Connection group icon -->
+    <div ng-show="item.balancing" class="icon type balancer"></div>
+
+    <!-- Connection group name -->
+    <span class="name">{{item.name}}</span>
+
+</a>