GUACAMOLE-903: Merge improved Chinese internationalization support

diff --git a/doc/guacamole-example/pom.xml b/doc/guacamole-example/pom.xml
index cf34bde..6db3c0f 100644
--- a/doc/guacamole-example/pom.xml
+++ b/doc/guacamole-example/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-example</artifactId>
     <packaging>war</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-example</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -106,7 +106,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.1.0</version>
+            <version>1.3.0</version>
             <scope>compile</scope>
         </dependency>
 
@@ -114,7 +114,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
diff --git a/doc/guacamole-playback-example/pom.xml b/doc/guacamole-playback-example/pom.xml
index e96f4f7..4c7739f 100644
--- a/doc/guacamole-playback-example/pom.xml
+++ b/doc/guacamole-playback-example/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-playback-example</artifactId>
     <packaging>war</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-playback-example</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -88,7 +88,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
diff --git a/extensions/guacamole-auth-cas/pom.xml b/extensions/guacamole-auth-cas/pom.xml
index 851001c..2ffe818 100644
--- a/extensions/guacamole-auth-cas/pom.xml
+++ b/extensions/guacamole-auth-cas/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-cas</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-cas</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -131,7 +131,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -183,6 +183,26 @@
             <scope>provided</scope>
         </dependency>
 
+        <!-- JUnit -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>5.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <version>5.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>5.6.0</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java
index 9f171e8..8150f97 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java
@@ -20,9 +20,7 @@
 package org.apache.guacamole.auth.cas;
 
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import java.util.Arrays;
-import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.form.Field;
 import org.apache.guacamole.GuacamoleException;
@@ -54,12 +52,6 @@
     private TicketValidationService ticketService;
 
     /**
-     * Provider for AuthenticatedUser objects.
-     */
-    @Inject
-    private Provider<CASAuthenticatedUser> authenticatedUserProvider;
-
-    /**
      * Returns an AuthenticatedUser representing the user authenticated by the
      * given credentials.
      *
@@ -82,13 +74,7 @@
         if (request != null) {
             String ticket = request.getParameter(CASTicketField.PARAMETER_NAME);
             if (ticket != null) {
-                Map<String, String> tokens = ticketService.validateTicket(ticket, credentials);
-                String username = credentials.getUsername();
-                if (username != null) {
-                    CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
-                    authenticatedUser.init(username, credentials, tokens);
-                    return authenticatedUser;
-                }
+                return ticketService.validateTicket(ticket, credentials);
             }
         }
 
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java
index 2ee42db..7bb363f 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java
@@ -19,7 +19,10 @@
 
 package org.apache.guacamole.auth.cas.conf;
 
+import org.apache.guacamole.auth.cas.group.GroupFormat;
+import org.apache.guacamole.properties.EnumGuacamoleProperty;
 import org.apache.guacamole.properties.URIGuacamoleProperty;
+import org.apache.guacamole.properties.StringGuacamoleProperty;
 
 /**
  * Provides properties required for use of the CAS authentication provider.
@@ -68,5 +71,51 @@
         public String getName() { return "cas-clearpass-key"; }
 
     };
+  
+    /**
+     * The name of the CAS attribute used for group membership, such as
+     * "memberOf". This attribute is case sensitive.
+     */
+    public static final StringGuacamoleProperty CAS_GROUP_ATTRIBUTE =
+            new StringGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "cas-group-attribute"; }
+
+    };
+
+    /**
+     * The format used by CAS to represent group names. Possible formats are
+     * "plain" (simple text names) or "ldap" (fully-qualified LDAP DNs).
+     */
+    public static final EnumGuacamoleProperty<GroupFormat> CAS_GROUP_FORMAT =
+            new EnumGuacamoleProperty<GroupFormat>(GroupFormat.class) {
+
+        @Override
+        public String getName() { return "cas-group-format"; }
+
+    };
+
+    /**
+     * The LDAP base DN to require for all CAS groups.
+     */
+    public static final LdapNameGuacamoleProperty CAS_GROUP_LDAP_BASE_DN =
+            new LdapNameGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "cas-group-ldap-base-dn"; }
+
+    };
+
+    /**
+     * The LDAP attribute to require for the names of CAS groups.
+     */
+    public static final StringGuacamoleProperty CAS_GROUP_LDAP_ATTRIBUTE =
+            new StringGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "cas-group-ldap-attribute"; }
+
+    };
 
 }
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java
index 680f170..ce5edd8 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java
@@ -22,8 +22,14 @@
 import com.google.inject.Inject;
 import java.net.URI;
 import java.security.PrivateKey;
+import javax.naming.ldap.LdapName;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.cas.group.GroupFormat;
 import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.auth.cas.group.GroupParser;
+import org.apache.guacamole.auth.cas.group.LDAPGroupParser;
+import org.apache.guacamole.auth.cas.group.PlainGroupParser;
 
 /**
  * Service for retrieving configuration information regarding the CAS service.
@@ -85,4 +91,102 @@
         return environment.getProperty(CASGuacamoleProperties.CAS_CLEARPASS_KEY);
     }
 
+    /**
+     * Returns the CAS attribute that should be used to determine group
+     * memberships in CAS, such as "memberOf". If no attribute has been
+     * specified, null is returned.
+     *
+     * @return
+     *     The attribute name used to determine group memberships in CAS,
+     *     null if not defined.
+     *
+     * @throws GuacamoleException
+     *     If guacamole.properties cannot be parsed.
+     */
+    public String getGroupAttribute() throws GuacamoleException {
+        return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_ATTRIBUTE);
+    }
+
+    /**
+     * Returns the format that CAS is expected to use for its group names, such
+     * as {@link GroupFormat#PLAIN} (simple plain-text names) or
+     * {@link GroupFormat#LDAP} (fully-qualified LDAP DNs). If not specified,
+     * PLAIN is used by default.
+     *
+     * @return
+     *     The format that CAS is expected to use for its group names.
+     *
+     * @throws GuacamoleException
+     *     If the format specified within guacamole.properties is not valid.
+     */
+    public GroupFormat getGroupFormat() throws GuacamoleException {
+        return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_FORMAT, GroupFormat.PLAIN);
+    }
+
+    /**
+     * Returns the base DN that all LDAP-formatted CAS groups must reside
+     * beneath. Any groups that are not beneath this base DN should be ignored.
+     * If no such base DN is provided, the tree structure of the ancestors of
+     * LDAP-formatted CAS groups should not be considered.
+     *
+     * @return
+     *     The base DN that all LDAP-formatted CAS groups must reside beneath,
+     *     or null if the tree structure of the ancestors of LDAP-formatted
+     *     CAS groups should not be considered.
+     *
+     * @throws GuacamoleException
+     *     If the provided base DN is not a valid LDAP DN.
+     */
+    public LdapName getGroupLDAPBaseDN() throws GuacamoleException {
+        return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_BASE_DN);
+    }
+
+    /**
+     * Returns the LDAP attribute that should be required for all LDAP-formatted
+     * CAS groups. Any groups that do not use this attribute as the last
+     * (leftmost) attribute of their DN should be ignored. If no such LDAP
+     * attribute is provided, the last (leftmost) attribute should still be
+     * used to determine the group name, but the specific attribute involved
+     * should not be considered.
+     *
+     * @return
+     *     The LDAP attribute that should be required for all LDAP-formatted
+     *     CAS groups, or null if any attribute should be allowed.
+     *
+     * @throws GuacamoleException
+     *     If guacamole.properties cannot be parsed.
+     */
+    public String getGroupLDAPAttribute() throws GuacamoleException {
+        return environment.getProperty(CASGuacamoleProperties.CAS_GROUP_LDAP_ATTRIBUTE);
+    }
+
+    /**
+     * Returns a GroupParser instance that can be used to parse CAS group
+     * names. The parser returned will take into account the configured CAS
+     * group format, as well as any configured LDAP-specific restrictions.
+     *
+     * @return
+     *     A GroupParser instance that can be used to parse CAS group names as
+     *     configured in guacamole.properties.
+     *
+     * @throws GuacamoleException
+     *     If guacamole.properties cannot be parsed.
+     */
+    public GroupParser getGroupParser() throws GuacamoleException {
+        switch (getGroupFormat()) {
+
+            // Simple, plain-text groups
+            case PLAIN:
+                return new PlainGroupParser();
+
+            // LDAP DNs
+            case LDAP:
+                return new LDAPGroupParser(getGroupLDAPAttribute(), getGroupLDAPBaseDN());
+
+            default:
+                throw new GuacamoleServerException("Unsupported CAS group format: " + getGroupFormat());
+
+        }
+    }
+
 }
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/LdapNameGuacamoleProperty.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/LdapNameGuacamoleProperty.java
new file mode 100644
index 0000000..5469376
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/LdapNameGuacamoleProperty.java
@@ -0,0 +1,49 @@
+/*
+ * 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.cas.conf;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import org.apache.guacamole.properties.GuacamoleProperty;
+import org.apache.guacamole.GuacamoleServerException;
+
+/**
+ * A GuacamoleProperty whose value is an LDAP DN.
+ */
+public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty<LdapName>  {
+
+    @Override
+    public LdapName parseValue(String value) throws GuacamoleServerException {
+
+        // Consider null/empty values to be empty
+        if (value == null || value.isEmpty())
+            return null;
+
+        // Parse provided value as an LDAP DN
+        try {
+            return new LdapName(value);
+        }
+        catch (InvalidNameException e) {
+            throw new GuacamoleServerException("Invalid LDAP distinguished name.", e);
+        }
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupFormat.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupFormat.java
new file mode 100644
index 0000000..4e315da
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupFormat.java
@@ -0,0 +1,41 @@
+/*
+ * 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.cas.group;
+
+import org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue;
+
+/**
+ * Possible formats of group names received from CAS.
+ */
+public enum GroupFormat {
+
+    /**
+     * Simple, plain-text group names.
+     */
+    @PropertyValue("plain")
+    PLAIN,
+
+    /**
+     * Group names formatted as LDAP DNs.
+     */
+    @PropertyValue("ldap")
+    LDAP
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupParser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupParser.java
new file mode 100644
index 0000000..5c31d5d
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/GroupParser.java
@@ -0,0 +1,44 @@
+/*
+ * 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.cas.group;
+
+/**
+ * Parser which converts the group names returned by CAS into names usable by
+ * Guacamole. The format of a CAS group name may vary by the underlying
+ * authentication backend. For example, a CAS deployment backed by LDAP may
+ * provide group names as LDAP DNs, which must be transformed into normal group
+ * names to be usable within Guacamole.
+ *
+ * @see LDAPGroupParser
+ */
+public interface GroupParser {
+
+    /**
+     * Parses the given CAS group name into a group name usable by Guacamole.
+     *
+     * @param casGroup
+     *     The group name retrieved from CAS.
+     *
+     * @return
+     *     A group name usable by Guacamole, or null if the group is not valid.
+     */
+    String parse(String casGroup);
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/LDAPGroupParser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/LDAPGroupParser.java
new file mode 100644
index 0000000..9a33ef7
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/LDAPGroupParser.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cas.group;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GroupParser that converts group names from LDAP DNs into normal group names,
+ * using the last (leftmost) attribute of the DN as the name. Groups may
+ * optionally be restricted to only those beneath a specific base DN, or only
+ * those using a specific attribute as their last (leftmost) attribute.
+ */
+public class LDAPGroupParser implements GroupParser {
+
+    /**
+     * Logger for this class.
+     */
+    private static final Logger logger = LoggerFactory.getLogger(LDAPGroupParser.class);
+
+    /**
+     * The LDAP attribute to require for all accepted group names. If null, any
+     * LDAP attribute will be allowed.
+     */
+    private final String nameAttribute;
+
+    /**
+     * The base DN to require for all accepted group names. If null, ancestor
+     * tree structure will not be considered in accepting/rejecting a group.
+     */
+    private final LdapName baseDn;
+
+    /**
+     * Creates a new LDAPGroupParser which applies the given restrictions on
+     * any provided group names.
+     *
+     * @param nameAttribute
+     *     The LDAP attribute to require for all accepted group names. This
+     *     restriction applies to the last (leftmost) attribute only, which is
+     *     always used to determine the name of the group. If null, any LDAP
+     *     attribute will be allowed in the last (leftmost) position.
+     *
+     * @param baseDn
+     *     The base DN to require for all accepted group names. If null,
+     *     ancestor tree structure will not be considered in
+     *     accepting/rejecting a group.
+     */
+    public LDAPGroupParser(String nameAttribute, LdapName baseDn) {
+        this.nameAttribute = nameAttribute;
+        this.baseDn = baseDn;
+    }
+
+    @Override
+    public String parse(String casGroup) {
+
+        // Reject null/empty group names
+        if (casGroup == null || casGroup.isEmpty())
+            return null;
+
+        // Parse group as an LDAP DN
+        LdapName group;
+        try {
+            group = new LdapName(casGroup);
+        }
+        catch (InvalidNameException e) {
+            logger.debug("CAS group \"{}\" has been rejected as it is not a "
+                    + "valid LDAP DN.", casGroup, e);
+            return null;
+        }
+
+        // Reject any group that is not beneath the base DN
+        if (baseDn != null && !group.startsWith(baseDn))
+            return null;
+
+        // If a specific name attribute is defined, restrict to groups that
+        // use that attribute to distinguish themselves
+        Rdn last = group.getRdn(group.size() - 1);
+        if (nameAttribute != null && !nameAttribute.equalsIgnoreCase(last.getType()))
+            return null;
+
+        // The group name is the string value of the final attribute in the DN
+        return last.getValue().toString();
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/PlainGroupParser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/PlainGroupParser.java
new file mode 100644
index 0000000..04c6c05
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/group/PlainGroupParser.java
@@ -0,0 +1,32 @@
+/*
+ * 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.cas.group;
+
+/**
+ * GroupParser which simply passes through all CAS group names untouched.
+ */
+public class PlainGroupParser implements GroupParser {
+
+    @Override
+    public String parse(String casGroup) {
+        return casGroup;
+    }
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java
index fce4760..17ef923 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java
@@ -21,6 +21,7 @@
 
 import com.google.common.io.BaseEncoding;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.net.URI;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
@@ -30,13 +31,17 @@
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;
 import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleSecurityException;
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.auth.cas.conf.ConfigurationService;
+import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.token.TokenName;
 import org.jasig.cas.client.authentication.AttributePrincipal;
@@ -69,6 +74,46 @@
     private ConfigurationService confService;
 
     /**
+     * Provider for AuthenticatedUser objects.
+     */
+    @Inject
+    private Provider<CASAuthenticatedUser> authenticatedUserProvider;
+
+    /**
+     * Converts the given CAS attribute value object (whose type is variable)
+     * to a Set of String values. If the value is already a Collection of some
+     * kind, its values are converted to Strings and returned as the members of
+     * the Set. If the value is not already a Collection, it is assumed to be a
+     * single value, converted to a String, and used as the sole member of the
+     * set.
+     *
+     * @param obj
+     *     The CAS attribute value to convert to a Set of Strings.
+     *
+     * @return
+     *     A Set of all String values contained within the given CAS attribute
+     *     value.
+     */
+    private Set<String> toStringSet(Object obj) {
+
+        // Consider null to represent no provided values
+        if (obj == null)
+            return Collections.emptySet();
+
+        // If the provided object is already a Collection, produce a Collection
+        // where we know for certain that all values are Strings
+        if (obj instanceof Collection) {
+            return ((Collection<?>) obj).stream()
+                    .map(Object::toString)
+                    .collect(Collectors.toSet());
+        }
+
+        // Otherwise, assume we have only a single value
+        return Collections.singleton(obj.toString());
+
+    }
+
+    /**
      * Validates and parses the given ID ticket, returning a map of all
      * available tokens for the given user based on attributes provided by the
      * CAS server.  If the ticket is invalid an exception is thrown.
@@ -81,62 +126,80 @@
      *     password values in.
      *
      * @return
-     *     A Map all of tokens for the user parsed from attributes returned
-     *     by the CAS server.
+     *     A CASAuthenticatedUser instance containing the ticket data returned by the CAS server.
      *
      * @throws GuacamoleException
      *     If the ID ticket is not valid or guacamole.properties could
      *     not be parsed.
      */
-    public Map<String, String> validateTicket(String ticket,
+    public CASAuthenticatedUser validateTicket(String ticket,
             Credentials credentials) throws GuacamoleException {
 
-        // Retrieve the configured CAS URL, establish a ticket validator,
-        // and then attempt to validate the supplied ticket.  If that succeeds,
-        // grab the principal returned by the validator.
+        // Create a ticket validator that uses the configured CAS URL
         URI casServerUrl = confService.getAuthorizationEndpoint();
         Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrl.toString());
         validator.setAcceptAnyProxy(true);
         validator.setEncoding("UTF-8");
+
+        // Attempt to validate the supplied ticket
+        Assertion assertion;
         try {
-            Map<String, String> tokens = new HashMap<>();
             URI confRedirectURI = confService.getRedirectURI();
-            Assertion a = validator.validate(ticket, confRedirectURI.toString());
-            AttributePrincipal principal =  a.getPrincipal();
-            Map<String, Object> ticketAttrs =
-                    new HashMap<>(principal.getAttributes());
-
-            // Retrieve username and set the credentials.
-            String username = principal.getName();
-            if (username == null)
-                throw new GuacamoleSecurityException("No username provided by CAS.");
-            
-            credentials.setUsername(username);
-
-            // Retrieve password, attempt decryption, and set credentials.
-            Object credObj = ticketAttrs.remove("credential");
-            if (credObj != null) {
-                String clearPass = decryptPassword(credObj.toString());
-                if (clearPass != null && !clearPass.isEmpty())
-                    credentials.setPassword(clearPass);
-            }
-            
-            // Convert remaining attributes that have values to Strings
-            for (Entry <String, Object> attr : ticketAttrs.entrySet()) {
-                String tokenName = TokenName.canonicalize(attr.getKey(),
-                        CAS_ATTRIBUTE_TOKEN_PREFIX);
-                Object value = attr.getValue();
-                if (value != null)
-                    tokens.put(tokenName, value.toString());
-            }
-
-            return tokens;
-
-        } 
+            assertion = validator.validate(ticket, confRedirectURI.toString());
+        }
         catch (TicketValidationException e) {
             throw new GuacamoleException("Ticket validation failed.", e);
         }
 
+        // Pull user principal and associated attributes
+        AttributePrincipal principal =  assertion.getPrincipal();
+        Map<String, Object> ticketAttrs = new HashMap<>(principal.getAttributes());
+
+        // Retrieve user identity from principal
+        String username = principal.getName();
+        if (username == null)
+            throw new GuacamoleSecurityException("No username provided by CAS.");
+
+        // Update credentials with username provided by CAS for sake of
+        // ${GUAC_USERNAME} token
+        credentials.setUsername(username);
+
+        // Retrieve password, attempt decryption, and set credentials.
+        Object credObj = ticketAttrs.remove("credential");
+        if (credObj != null) {
+            String clearPass = decryptPassword(credObj.toString());
+            if (clearPass != null && !clearPass.isEmpty())
+                credentials.setPassword(clearPass);
+        }
+
+        Set<String> effectiveGroups;
+
+        // Parse effective groups from principal attributes if a specific
+        // group attribute has been configured
+        String groupAttribute = confService.getGroupAttribute();
+        if (groupAttribute != null) {
+            effectiveGroups = toStringSet(ticketAttrs.get(groupAttribute)).stream()
+                    .map(confService.getGroupParser()::parse)
+                    .collect(Collectors.toSet());
+        }
+
+        // Otherwise, assume no effective groups
+        else
+            effectiveGroups = Collections.emptySet();
+
+        // Convert remaining attributes that have values to Strings
+        Map<String, String> tokens = new HashMap<>(ticketAttrs.size());
+        ticketAttrs.forEach((key, value) -> {
+            if (value != null) {
+                String tokenName = TokenName.canonicalize(key, CAS_ATTRIBUTE_TOKEN_PREFIX);
+                tokens.put(tokenName, value.toString());
+            }
+        });
+
+        CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
+        authenticatedUser.init(username, credentials, tokens, effectiveGroups);
+        return authenticatedUser;
+
     }
 
     /**
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java
index 1b3a948..b79344e 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java
@@ -22,6 +22,7 @@
 import com.google.inject.Inject;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
@@ -51,6 +52,11 @@
     private Map<String, String> tokens;
 
     /**
+     * The unique identifiers of all user groups which this user is a member of.
+     */
+    private Set<String> effectiveGroups;
+
+    /**
      * Initializes this AuthenticatedUser using the given username and
      * credentials, and an empty map of parameter tokens.
      *
@@ -61,7 +67,7 @@
      *     The credentials provided when this user was authenticated.
      */
     public void init(String username, Credentials credentials) {
-        this.init(username, credentials, Collections.emptyMap());
+        this.init(username, credentials, Collections.emptyMap(), Collections.emptySet());
     }
     
     /**
@@ -79,9 +85,10 @@
      *     as tokens when connections are established with this user.
      */
     public void init(String username, Credentials credentials,
-            Map<String, String> tokens) {
+            Map<String, String> tokens, Set<String> effectiveGroups) {
         this.credentials = credentials;
         this.tokens = Collections.unmodifiableMap(tokens);
+        this.effectiveGroups = effectiveGroups;
         setIdentifier(username.toLowerCase());
     }
 
@@ -107,4 +114,9 @@
         return credentials;
     }
 
+    @Override
+    public Set<String> getEffectiveUserGroups() {
+        return effectiveGroups;
+    }
+
 }
diff --git a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
index 7869178..fe44a2e 100644
--- a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "CAS Authentication Extension",
     "namespace" : "guac-cas",
@@ -10,6 +10,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/ja.json",
diff --git a/extensions/guacamole-auth-cas/src/main/resources/translations/ca.json b/extensions/guacamole-auth-cas/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..36b99e7
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/resources/translations/ca.json
@@ -0,0 +1,11 @@
+{
+
+    "DATA_SOURCE_CAS" : {
+        "NAME" : "Backend d'inici de sessió unificat (SSO) CAS"
+    },
+
+    "LOGIN" : {
+        "INFO_CAS_REDIRECT_PENDING"  : "Espereu, redireccionant a l'autenticació CAS ..."
+    }
+
+}
diff --git a/extensions/guacamole-auth-cas/src/test/java/org/apache/guacamole/auth/cas/group/LDAPGroupParserTest.java b/extensions/guacamole-auth-cas/src/test/java/org/apache/guacamole/auth/cas/group/LDAPGroupParserTest.java
new file mode 100644
index 0000000..ffff0a7
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/test/java/org/apache/guacamole/auth/cas/group/LDAPGroupParserTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.cas.group;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test which confirms that the LDAPGroupParser implementation of GroupParser
+ * parses CAS groups correctly.
+ */
+public class LDAPGroupParserTest {
+
+    /**
+     * LdapName instance representing the LDAP DN: "dc=example,dc=net".
+     */
+    private final LdapName exampleBaseDn;
+
+    /**
+     * Creates a new LDAPGroupParserTest that verifies the functionality of
+     * LDAPGroupParser.
+     *
+     * @throws InvalidNameException
+     *     If the static string LDAP DN of any test instance of LdapName is
+     *     unexpectedly invalid.
+     */
+    public LDAPGroupParserTest() throws InvalidNameException {
+        exampleBaseDn = new LdapName("dc=example,dc=net");
+    }
+
+    /**
+     * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+     * when no restrictions are enforced on LDAP attributes or the base DN.
+     */
+    @Test
+    public void testParseRestrictNothing() {
+
+        GroupParser parser = new LDAPGroupParser(null, null);
+
+        // null input should be rejected as null
+        assertNull(parser.parse(null));
+
+        // Invalid DNs should be rejected as null
+        assertNull(parser.parse(""));
+        assertNull(parser.parse("foo"));
+
+        // Valid DNs should be accepted
+        assertEquals("bar", parser.parse("foo=bar"));
+        assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
+        assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
+        assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
+        assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+        assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+
+    }
+
+    /**
+     * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+     * when restrictions are enforced on LDAP attributes only.
+     */
+    @Test
+    public void testParseRestrictAttribute() {
+
+        GroupParser parser = new LDAPGroupParser("cn", null);
+
+        // null input should be rejected as null
+        assertNull(parser.parse(null));
+
+        // Invalid DNs should be rejected as null
+        assertNull(parser.parse(""));
+        assertNull(parser.parse("foo"));
+
+        // Valid DNs not using the correct attribute should be rejected as null
+        assertNull(parser.parse("foo=bar"));
+        assertNull(parser.parse("ou=baz,dc=example,dc=com"));
+        assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=com"));
+
+        // Valid DNs using the correct attribute should be accepted
+        assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+        assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+        assertEquals("baz", parser.parse("CN=baz,dc=example,dc=com"));
+
+    }
+
+    /**
+     * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+     * when restrictions are enforced on the LDAP base DN only.
+     */
+    @Test
+    public void testParseRestrictBaseDN() {
+
+        GroupParser parser = new LDAPGroupParser(null, exampleBaseDn);
+
+        // null input should be rejected as null
+        assertNull(parser.parse(null));
+
+        // Invalid DNs should be rejected as null
+        assertNull(parser.parse(""));
+        assertNull(parser.parse("foo"));
+
+        // Valid DNs outside the base DN should be rejected as null
+        assertNull(parser.parse("foo=bar"));
+        assertNull(parser.parse("CN=baz,dc=example,dc=com"));
+
+        // Valid DNs beneath the base DN should be accepted
+        assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+        assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+        assertEquals("baz", parser.parse("ou=baz,dc=example,dc=net"));
+        assertEquals("foo", parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
+
+    }
+
+    /**
+     * Verifies that LDAPGroupParser correctly parses LDAP-based CAS groups
+     * when restrictions are enforced on both LDAP attributes and the base DN.
+     */
+    @Test
+    public void testParseRestrictAll() {
+
+        GroupParser parser = new LDAPGroupParser("cn", exampleBaseDn);
+
+        // null input should be rejected as null
+        assertNull(parser.parse(null));
+
+        // Invalid DNs should be rejected as null
+        assertNull(parser.parse(""));
+        assertNull(parser.parse("foo"));
+
+        // Valid DNs outside the base DN should be rejected as null
+        assertNull(parser.parse("foo=bar"));
+        assertNull(parser.parse("CN=baz,dc=example,dc=com"));
+
+        // Valid DNs beneath the base DN but not using the correct attribute
+        // should be rejected as null
+        assertNull(parser.parse("ou=baz,dc=example,dc=net"));
+        assertNull(parser.parse("ou=foo,cn=baz,dc=example,dc=net"));
+
+        // Valid DNs beneath the base DN and using the correct attribute should
+        // be accepted
+        assertEquals("foo", parser.parse("cn=foo,DC=example,dc=net"));
+        assertEquals("bar", parser.parse("CN=bar,OU=groups,dc=example,Dc=net"));
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-duo/pom.xml b/extensions/guacamole-auth-duo/pom.xml
index 6154be7..240fcab 100644
--- a/extensions/guacamole-auth-duo/pom.xml
+++ b/extensions/guacamole-auth-duo/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-duo</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-duo</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -213,7 +213,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
         
diff --git a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
index 05c5928..44c7530 100644
--- a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "Duo TFA Authentication Backend",
     "namespace" : "duo",
@@ -10,6 +10,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/ja.json",
diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/ca.json b/extensions/guacamole-auth-duo/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..c154ae6
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/translations/ca.json
@@ -0,0 +1,13 @@
+{
+
+    "DATA_SOURCE_DUO" : {
+        "NAME" : "Duo TFA Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "",
+        "INFO_DUO_VALIDATION_CODE_INCORRECT"    : "El codi de validació duo és incorrecte.",
+        "INFO_DUO_AUTH_REQUIRED"                : "Si us plau, autentiqueu amb Duo per continuar."
+    }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
index e6df64c..b052ee7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.2.0</version>
+        <version>1.3.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java
index d259018..60446aa 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledActivityRecordSet.java
@@ -48,7 +48,7 @@
      * strings within the collection will be excluded from the results.
      */
     private final Set<ActivityRecordSearchTerm> requiredContents =
-            new HashSet<ActivityRecordSearchTerm>();
+            new HashSet<>();
     
     /**
      * The maximum number of history records that should be returned by a call
@@ -62,7 +62,7 @@
      * properties.
      */
     private final List<ActivityRecordSortPredicate> sortPredicates =
-            new ArrayList<ActivityRecordSortPredicate>();
+            new ArrayList<>();
 
     /**
      * Retrieves the history records matching the given criteria. Retrieves up
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java
index 7380b21..720ded3 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.java
@@ -62,8 +62,12 @@
      * the data they are associated with is is readable by any particular user.
      * This should only be called on behalf of a system administrator. If
      * records are needed by a non-administrative user who must have explicit
-     * read rights, use searchReadable() instead.
+     * read rights, use {@link searchReadable()} instead.
      *
+     * @param identifier
+     *     The optional connection identifier to which records should be limited,
+     *     or null if all records should be retrieved.
+     * 
      * @param terms
      *     The search terms that must match the returned records.
      *
@@ -77,7 +81,8 @@
      * @return
      *     The results of the search performed with the given parameters.
      */
-    List<ConnectionRecordModel> search(@Param("terms") Collection<ActivityRecordSearchTerm> terms,
+    List<ConnectionRecordModel> search(@Param("identifier") String identifier,
+            @Param("terms") Collection<ActivityRecordSearchTerm> terms,
             @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
             @Param("limit") int limit);
 
@@ -86,8 +91,13 @@
      * the given terms, sorted by the given predicates. Only records that are
      * associated with data explicitly readable by the given user will be
      * returned. If records are needed by a system administrator (who, by
-     * definition, does not need explicit read rights), use search() instead.
+     * definition, does not need explicit read rights), use {@link search()}
+     * instead.
      *
+     * @param identifier
+     *     The optional connection identifier for which records should be
+     *     retrieved, or null if all readable records should be retrieved.
+     * 
      * @param user
      *    The user whose permissions should determine whether a record is
      *    returned.
@@ -111,7 +121,8 @@
      * @return
      *     The results of the search performed with the given parameters.
      */
-    List<ConnectionRecordModel> searchReadable(@Param("user") UserModel user,
+    List<ConnectionRecordModel> searchReadable(@Param("identifier") String identifier,
+            @Param("user") UserModel user,
             @Param("terms") Collection<ActivityRecordSearchTerm> terms,
             @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
             @Param("limit") int limit,
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java
index f4574f4..2f559e9 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordSet.java
@@ -27,6 +27,7 @@
 import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
 import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
 import org.apache.guacamole.auth.jdbc.base.ModeledActivityRecordSet;
+import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.ConnectionRecord;
 
@@ -43,6 +44,30 @@
     @Inject
     private ConnectionService connectionService;
     
+    /**
+     * The identifier of the connection to which this record set should be
+     * limited, if any. If null, the set should contain all records readable
+     * by the user making the request.
+     */
+    private String identifier = null;
+    
+    /**
+     * Initializes this object, associating it with the current authenticated
+     * user and connection identifier.
+     *
+     * @param currentUser
+     *     The user that created or retrieved this object.
+     * 
+     * @param identifier
+     *     The connection identifier to which this record set should be limited,
+     *     or null if the record set should contain all records readable by the
+     *     currentUser.
+     */
+    protected void init(ModeledAuthenticatedUser currentUser, String identifier) {
+        super.init(currentUser);
+        this.identifier = identifier;
+    }
+    
     @Override
     protected Collection<ConnectionRecord> retrieveHistory(
             AuthenticatedUser user, Set<ActivityRecordSearchTerm> requiredContents,
@@ -50,7 +75,7 @@
             throws GuacamoleException {
 
         // Retrieve history from database
-        return connectionService.retrieveHistory(getCurrentUser(),
+        return connectionService.retrieveHistory(identifier, getCurrentUser(),
                 requiredContents, sortPredicates, limit);
 
     }
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 926df32..16d3784 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
@@ -213,7 +213,7 @@
         Map<String, String> parameters = connection.getConfiguration().getParameters();
         
         // Convert parameters to model objects
-        Collection<ConnectionParameterModel> parameterModels = new ArrayList<ConnectionParameterModel>(parameters.size());
+        Collection<ConnectionParameterModel> parameterModels = new ArrayList<>(parameters.size());
         for (Map.Entry<String, String> parameterEntry : parameters.entrySet()) {
 
             // Get parameter name and value
@@ -329,7 +329,7 @@
     public Map<String, String> retrieveParameters(ModeledAuthenticatedUser user,
             String identifier) {
 
-        Map<String, String> parameterMap = new HashMap<String, String>();
+        Map<String, String> parameterMap = new HashMap<>();
 
         // Determine whether we have permission to read parameters
         boolean canRetrieveParameters;
@@ -382,7 +382,7 @@
     protected List<ConnectionRecord> getObjectInstances(List<ConnectionRecordModel> models) {
 
         // Create new list of records by manually converting each model
-        List<ConnectionRecord> objects = new ArrayList<ConnectionRecord>(models.size());
+        List<ConnectionRecord> objects = new ArrayList<>(models.size());
         for (ConnectionRecordModel model : models)
             objects.add(getObjectInstance(model));
 
@@ -411,28 +411,16 @@
             ModeledConnection connection) throws GuacamoleException {
 
         String identifier = connection.getIdentifier();
-
-        // Retrieve history only if READ permission is granted
-        if (hasObjectPermission(user, identifier, ObjectPermission.Type.READ)) {
-
-            // Retrieve history
-            List<ConnectionRecordModel> models = connectionRecordMapper.select(identifier);
-
-            // Get currently-active connections
-            List<ConnectionRecord> records = new ArrayList<ConnectionRecord>(tunnelService.getActiveConnections(connection));
-            Collections.reverse(records);
-
-            // Add past connections from model objects
-            for (ConnectionRecordModel model : models)
-                records.add(getObjectInstance(model));
-
-            // Return converted history list
-            return records;
-
-        }
-
-        // The user does not have permission to read the history
-        throw new GuacamoleSecurityException("Permission denied.");
+        
+        // Get current active connections.
+        List<ConnectionRecord> records = new ArrayList<>(tunnelService.getActiveConnections(connection));
+        Collections.reverse(records);
+        
+        // Add in the history records.
+        records.addAll(retrieveHistory(identifier, user, Collections.emptyList(),
+                Collections.emptyList(), Integer.MAX_VALUE));
+        
+        return records;
 
     }
 
@@ -442,6 +430,60 @@
      * the given terms and sorted by the given predicates. Only history records
      * associated with data that the given user can read are returned.
      *
+     * @param identifier
+     *     The optional connection identifier for which history records should
+     *     be retrieved, or null if all readable records should be retrieved.
+     * 
+     * @param user
+     *     The user retrieving the connection history.
+     *
+     * @param requiredContents
+     *     The search terms that must be contained somewhere within each of the
+     *     returned records.
+     *
+     * @param sortPredicates
+     *     A list of predicates to sort the returned records by, in order of
+     *     priority.
+     *
+     * @param limit
+     *     The maximum number of records that should be returned.
+     *
+     * @return
+     *     The connection history of the given connection, including any
+     *     active connections.
+     *
+     * @throws GuacamoleException
+     *     If permission to read the connection history is denied.
+     */
+    public List<ConnectionRecord> retrieveHistory(String identifier,
+            ModeledAuthenticatedUser user,
+            Collection<ActivityRecordSearchTerm> requiredContents,
+            List<ActivityRecordSortPredicate> sortPredicates, int limit)
+            throws GuacamoleException {
+
+        List<ConnectionRecordModel> searchResults;
+
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
+            searchResults = connectionRecordMapper.search(identifier, requiredContents,
+                    sortPredicates, limit);
+
+        // Otherwise only return explicitly readable history records
+        else
+            searchResults = connectionRecordMapper.searchReadable(identifier,
+                    user.getUser().getModel(), requiredContents, sortPredicates,
+                    limit, user.getEffectiveUserGroups());
+
+        return getObjectInstances(searchResults);
+
+    }
+    
+    /**
+     * Retrieves the connection history records matching the given criteria.
+     * Retrieves up to <code>limit</code> connection history records matching
+     * the given terms and sorted by the given predicates. Only history records
+     * associated with data that the given user can read are returned.
+     * 
      * @param user
      *     The user retrieving the connection history.
      *
@@ -467,22 +509,9 @@
             Collection<ActivityRecordSearchTerm> requiredContents,
             List<ActivityRecordSortPredicate> sortPredicates, int limit)
             throws GuacamoleException {
-
-        List<ConnectionRecordModel> searchResults;
-
-        // Bypass permission checks if the user is privileged
-        if (user.isPrivileged())
-            searchResults = connectionRecordMapper.search(requiredContents,
-                    sortPredicates, limit);
-
-        // Otherwise only return explicitly readable history records
-        else
-            searchResults = connectionRecordMapper.searchReadable(
-                    user.getUser().getModel(), requiredContents, sortPredicates,
-                    limit, user.getEffectiveUserGroups());
-
-        return getObjectInstances(searchResults);
-
+        
+        return retrieveHistory(null, user, requiredContents, sortPredicates, limit);
+        
     }
 
     /**
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
index b492626..20861ba 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
@@ -40,6 +40,7 @@
 import org.apache.guacamole.form.NumericField;
 import org.apache.guacamole.form.TextField;
 import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
 import org.apache.guacamole.net.auth.Connection;
 import org.apache.guacamole.net.auth.ConnectionRecord;
 import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration;
@@ -197,6 +198,12 @@
     private Provider<ModeledGuacamoleConfiguration> configProvider;
     
     /**
+     * Provider for creating connection record sets.
+     */
+    @Inject
+    private Provider<ConnectionRecordSet> connectionRecordSetProvider;
+    
+    /**
      * The manually-set GuacamoleConfiguration, if any.
      */
     private GuacamoleConfiguration config = null;
@@ -252,10 +259,13 @@
     public Date getLastActive() {
         return getModel().getLastActive();
     }
-
+    
     @Override
-    public List<? extends ConnectionRecord> getHistory() throws GuacamoleException {
-        return connectionService.retrieveHistory(getCurrentUser(), this);
+    public ActivityRecordSet<ConnectionRecord> getConnectionHistory()
+            throws GuacamoleException {
+        ConnectionRecordSet connectionRecordSet = connectionRecordSetProvider.get();
+        connectionRecordSet.init(getCurrentUser(), this.getIdentifier());
+        return connectionRecordSet;
     }
 
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnection.java
index cf00831..48a0cbd 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnection.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnection.java
@@ -22,7 +22,6 @@
 import com.google.inject.Inject;
 import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.guacamole.GuacamoleException;
@@ -31,7 +30,6 @@
 import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.net.auth.Connection;
-import org.apache.guacamole.net.auth.ConnectionRecord;
 import org.apache.guacamole.protocol.GuacamoleClientInformation;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
 
@@ -153,12 +151,6 @@
     }
 
     @Override
-    public List<? extends ConnectionRecord> getHistory()
-            throws GuacamoleException {
-        return Collections.<ConnectionRecord>emptyList();
-    }
-
-    @Override
     public Set<String> getSharingProfileIdentifiers()
             throws GuacamoleException {
         return Collections.<String>emptySet();
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedUser.java
index f0a48b8..80d2219 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedUser.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/user/SharedUser.java
@@ -21,11 +21,9 @@
 
 import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.auth.jdbc.sharing.permission.SharedObjectPermissionSet;
-import org.apache.guacamole.net.auth.ActivityRecord;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.Connection;
 import org.apache.guacamole.net.auth.ConnectionGroup;
@@ -100,14 +98,6 @@
     }
 
     @Override
-    public List<ActivityRecord> getHistory() throws GuacamoleException {
-
-        // History is not recorded for shared users
-        return Collections.<ActivityRecord>emptyList();
-
-    }
-
-    @Override
     public String getPassword() {
         return null;
     }
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 6687eb3..44b65bf 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
@@ -201,87 +201,52 @@
             ModeledConnectionGroup connectionGroup);
 
     /**
-     * Returns a guacamole configuration containing the protocol and parameters
-     * from the given connection. If the ID of an active connection is
-     * provided, that connection will be joined instead of starting a new
-     * primary connection. If tokens are used in the connection parameter
-     * values, credentials from the given user will be substituted
-     * appropriately.
-     *
-     * @param user
-     *     The user whose credentials should be used if necessary.
+     * Returns a GuacamoleConfiguration which connects to the given connection.
+     * If the ID of an active connection is provided, that active connection
+     * will be joined rather than establishing an entirely new connection. If
+     * a sharing profile is provided, the parameters associated with that
+     * sharing profile will be used to define the access provided to the user
+     * accessing the shared connection.
      *
      * @param connection
-     *     The connection whose protocol and parameters should be added to the
-     *     returned configuration.
+     *     The connection that the user is connecting to.
      *
      * @param connectionID
-     *     The ID of the active connection to be joined, as returned by guacd,
-     *     or null if a new primary connection should be established.
-     *
-     * @return
-     *     A GuacamoleConfiguration containing the protocol and parameters from
-     *     the given connection.
-     */
-    private GuacamoleConfiguration getGuacamoleConfiguration(RemoteAuthenticatedUser user,
-            ModeledConnection connection, String connectionID) {
-
-        // Generate configuration from available data
-        GuacamoleConfiguration config = new GuacamoleConfiguration();
-
-        // Join existing active connection, if any
-        if (connectionID != null)
-            config.setConnectionID(connectionID);
-
-        // Set protocol from connection if not joining an active connection
-        else {
-            ConnectionModel model = connection.getModel();
-            config.setProtocol(model.getProtocol());
-        }
-
-        // Set parameters from associated data
-        Collection<ConnectionParameterModel> parameters = connectionParameterMapper.select(connection.getIdentifier());
-        for (ConnectionParameterModel parameter : parameters)
-            config.setParameter(parameter.getName(), parameter.getValue());
-
-        return config;
-        
-    }
-
-    /**
-     * Returns a guacamole configuration which joins the active connection
-     * having the given ID, using the provided sharing profile to restrict the
-     * access provided to the user accessing the shared connection. If tokens
-     * are used in the connection parameter values of the sharing profile,
-     * credentials from the given user will be substituted appropriately.
-     *
-     * @param user
-     *     The user whose credentials should be used if necessary.
+     *     The ID of the active connection being joined, as provided by guacd
+     *     when the original connection was established, or null if a new
+     *     connection should be established instead.
      *
      * @param sharingProfile
      *     The sharing profile whose associated parameters dictate the level
-     *     of access granted to the user joining the connection.
-     *
-     * @param connectionID
-     *     The ID of the connection being joined, as provided by guacd when the
-     *     original connection was established, or null if a new connection
-     *     should be created instead.
+     *     of access granted to the user joining the connection, or null if the
+     *     parameters associated with the connection should be used.
      *
      * @return
-     *     A GuacamoleConfiguration containing the protocol and parameters from
-     *     the given connection.
+     *     A GuacamoleConfiguration defining the requested, possibly shared
+     *     connection.
      */
-    private GuacamoleConfiguration getGuacamoleConfiguration(RemoteAuthenticatedUser user,
-            ModeledSharingProfile sharingProfile, String connectionID) {
+    private GuacamoleConfiguration getGuacamoleConfiguration(
+            ModeledConnection connection, String connectionID,
+            ModeledSharingProfile sharingProfile) {
+
+        ConnectionModel model = connection.getModel();
 
         // Generate configuration from available data
         GuacamoleConfiguration config = new GuacamoleConfiguration();
+        config.setProtocol(model.getProtocol());
         config.setConnectionID(connectionID);
 
         // Set parameters from associated data
-        Collection<SharingProfileParameterModel> parameters = sharingProfileParameterMapper.select(sharingProfile.getIdentifier());
-        for (SharingProfileParameterModel parameter : parameters)
-            config.setParameter(parameter.getName(), parameter.getValue());
+        if (sharingProfile != null) {
+            Collection<SharingProfileParameterModel> parameters = sharingProfileParameterMapper.select(sharingProfile.getIdentifier());
+            for (SharingProfileParameterModel parameter : parameters)
+                config.setParameter(parameter.getName(), parameter.getValue());
+        }
+        else {
+            Collection<ConnectionParameterModel> parameters = connectionParameterMapper.select(connection.getIdentifier());
+            for (ConnectionParameterModel parameter : parameters)
+                config.setParameter(parameter.getName(), parameter.getValue());
+        }
 
         return config;
 
@@ -488,7 +453,7 @@
             if (activeConnection.isPrimaryConnection()) {
                 activeConnections.put(connection.getIdentifier(), activeConnection);
                 activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection);
-                config = getGuacamoleConfiguration(activeConnection.getUser(), connection, activeConnection.getConnectionID());
+                config = getGuacamoleConfiguration(connection, activeConnection.getConnectionID(), null);
             }
 
             // If we ARE joining an active connection under the restrictions of
@@ -502,8 +467,7 @@
 
                 // Build configuration from the sharing profile and the ID of
                 // the connection being joined
-                config = getGuacamoleConfiguration(activeConnection.getUser(),
-                        activeConnection.getSharingProfile(), connectionID);
+                config = getGuacamoleConfiguration(connection, connectionID, activeConnection.getSharingProfile());
 
             }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java
index 54f052b..fe04cec 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUser.java
@@ -47,6 +47,7 @@
 import org.apache.guacamole.form.TimeField;
 import org.apache.guacamole.form.TimeZoneField;
 import org.apache.guacamole.net.auth.ActivityRecord;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
 import org.apache.guacamole.net.auth.Permissions;
 import org.apache.guacamole.net.auth.RelatedObjectSet;
 import org.apache.guacamole.net.auth.User;
@@ -182,6 +183,12 @@
      */
     @Inject
     private Provider<UserParentUserGroupSet> parentUserGroupSetProvider;
+    
+    /**
+     * Provider for creating user record sets.
+     */
+    @Inject
+    private Provider<UserRecordSet> userRecordSetProvider;
 
     /**
      * Whether attributes which control access restrictions should be exposed
@@ -748,8 +755,11 @@
     }
 
     @Override
-    public List<ActivityRecord> getHistory() throws GuacamoleException {
-        return userService.retrieveHistory(getCurrentUser(), this);
+    public ActivityRecordSet<ActivityRecord> getUserHistory()
+            throws GuacamoleException {
+        UserRecordSet userRecordSet = userRecordSetProvider.get();
+        userRecordSet.init(getCurrentUser(), this.getIdentifier());
+        return userRecordSet;
     }
 
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java
index 92501ab..fe15f41 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.java
@@ -73,8 +73,12 @@
      * the data they are associated with is is readable by any particular user.
      * This should only be called on behalf of a system administrator. If
      * records are needed by a non-administrative user who must have explicit
-     * read rights, use searchReadable() instead.
+     * read rights, use {@link searchReadable()} instead.
      *
+     * @param username
+     *     The optional username to which records should be limited, or null
+     *     if all records should be retrieved.
+     * 
      * @param terms
      *     The search terms that must match the returned records.
      *
@@ -88,7 +92,8 @@
      * @return
      *     The results of the search performed with the given parameters.
      */
-    List<ActivityRecordModel> search(@Param("terms") Collection<ActivityRecordSearchTerm> terms,
+    List<ActivityRecordModel> search(@Param("username") String username,
+            @Param("terms") Collection<ActivityRecordSearchTerm> terms,
             @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
             @Param("limit") int limit);
 
@@ -97,11 +102,16 @@
      * the given terms, sorted by the given predicates. Only records that are
      * associated with data explicitly readable by the given user will be
      * returned. If records are needed by a system administrator (who, by
-     * definition, does not need explicit read rights), use search() instead.
+     * definition, does not need explicit read rights), use {@link search()}
+     * instead.
      *
+     * @param username
+     *     The optional username to which records should be limited, or null
+     *     if all readable records should be retrieved.
+     * 
      * @param user
-     *    The user whose permissions should determine whether a record is
-     *    returned.
+     *     The user whose permissions should determine whether a record is
+     *     returned.
      *
      * @param terms
      *     The search terms that must match the returned records.
@@ -122,7 +132,8 @@
      * @return
      *     The results of the search performed with the given parameters.
      */
-    List<ActivityRecordModel> searchReadable(@Param("user") UserModel user,
+    List<ActivityRecordModel> searchReadable(@Param("username") String username,
+            @Param("user") UserModel user,
             @Param("terms") Collection<ActivityRecordSearchTerm> terms,
             @Param("sortPredicates") List<ActivityRecordSortPredicate> sortPredicates,
             @Param("limit") int limit,
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java
index c1b4897..c5c9f6a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserRecordSet.java
@@ -44,6 +44,31 @@
     @Inject
     private UserService userService;
     
+    /**
+     * The identifier that indicates which user object these records should be
+     * limited to, if any. If null is specified (the default) then all records
+     * that are readable by the current user will be retrieved.
+     */
+    private String identifier = null;
+    
+    /**
+     * Initialize this UserRecordSet with currentUser requesting the login
+     * records, and, optionally, the identifier of the user to which records
+     * should be limited.
+     * 
+     * @param currentUser
+     *     The user requesting login history.
+     * 
+     * @param identifier 
+     *     The identifier of the user whose login history should be contained
+     *     in this record set, or null if the record set should contain all
+     *     records readable by the currentUser.
+     */
+    protected void init(ModeledAuthenticatedUser currentUser, String identifier) {
+        super.init(currentUser);
+        this.identifier = identifier;
+    }
+    
     @Override
     protected Collection<ActivityRecord> retrieveHistory(
             AuthenticatedUser user, Set<ActivityRecordSearchTerm> requiredContents,
@@ -51,7 +76,7 @@
             throws GuacamoleException {
 
         // Retrieve history from database
-        return userService.retrieveHistory(getCurrentUser(),
+        return userService.retrieveHistory(identifier, getCurrentUser(),
                 requiredContents, sortPredicates, limit);
 
     }
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 d22e909..6f6d056 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
@@ -32,7 +32,6 @@
 import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectService;
 import org.apache.guacamole.GuacamoleClientException;
 import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.GuacamoleSecurityException;
 import org.apache.guacamole.GuacamoleUnsupportedException;
 import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
 import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
@@ -593,13 +592,9 @@
             ModeledUser user) throws GuacamoleException {
 
         String username = user.getIdentifier();
-
-        // Retrieve history only if READ permission is granted
-        if (hasObjectPermission(authenticatedUser, username, ObjectPermission.Type.READ))
-            return getObjectInstances(userRecordMapper.select(username));
-
-        // The user does not have permission to read the history
-        throw new GuacamoleSecurityException("Permission denied.");
+        
+        return retrieveHistory(username, authenticatedUser, Collections.emptyList(),
+                Collections.emptyList(), Integer.MAX_VALUE);
 
     }
 
@@ -609,6 +604,59 @@
      * given terms and sorted by the given predicates. Only history records
      * associated with data that the given user can read are returned.
      *
+     * @param username
+     *     The optional username to which history records should be limited, or
+     *     null if all readable records should be retrieved.
+     * 
+     * @param user
+     *     The user retrieving the login history.
+     *
+     * @param requiredContents
+     *     The search terms that must be contained somewhere within each of the
+     *     returned records.
+     *
+     * @param sortPredicates
+     *     A list of predicates to sort the returned records by, in order of
+     *     priority.
+     *
+     * @param limit
+     *     The maximum number of records that should be returned.
+     *
+     * @return
+     *     The login history of the given user, including any active sessions.
+     *
+     * @throws GuacamoleException
+     *     If permission to read the user login history is denied.
+     */
+    public List<ActivityRecord> retrieveHistory(String username,
+            ModeledAuthenticatedUser user,
+            Collection<ActivityRecordSearchTerm> requiredContents,
+            List<ActivityRecordSortPredicate> sortPredicates, int limit)
+            throws GuacamoleException {
+
+        List<ActivityRecordModel> searchResults;
+
+        // Bypass permission checks if the user is privileged
+        if (user.isPrivileged())
+            searchResults = userRecordMapper.search(username, requiredContents,
+                    sortPredicates, limit);
+
+        // Otherwise only return explicitly readable history records
+        else
+            searchResults = userRecordMapper.searchReadable(username, 
+                    user.getUser().getModel(),
+                    requiredContents, sortPredicates, limit, user.getEffectiveUserGroups());
+
+        return getObjectInstances(searchResults);
+
+    }
+    
+    /**
+     * Retrieves user login history records matching the given criteria.
+     * Retrieves up to <code>limit</code> user history records matching the
+     * given terms and sorted by the given predicates. Only history records
+     * associated with data that the given user can read are returned.
+     * 
      * @param user
      *     The user retrieving the login history.
      *
@@ -633,21 +681,9 @@
             Collection<ActivityRecordSearchTerm> requiredContents,
             List<ActivityRecordSortPredicate> sortPredicates, int limit)
             throws GuacamoleException {
-
-        List<ActivityRecordModel> searchResults;
-
-        // Bypass permission checks if the user is privileged
-        if (user.isPrivileged())
-            searchResults = userRecordMapper.search(requiredContents,
-                    sortPredicates, limit);
-
-        // Otherwise only return explicitly readable history records
-        else
-            searchResults = userRecordMapper.searchReadable(user.getUser().getModel(),
-                    requiredContents, sortPredicates, limit, user.getEffectiveUserGroups());
-
-        return getObjectInstances(searchResults);
-
+        
+        return retrieveHistory(null, user, requiredContents, sortPredicates, limit);
+        
     }
 
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/ca.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..64b2723
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/ca.json
@@ -0,0 +1,113 @@
+{
+
+    "LOGIN" : {
+
+        "ERROR_PASSWORD_BLANK"    : "@:APP.ERROR_PASSWORD_BLANK",
+        "ERROR_PASSWORD_SAME"     : "La nova contrasenya ha de ser diferent de la contrasenya caducada.",
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+        "ERROR_NOT_VALID"         : "Aquest compte d'usuari actualment no és vàlid.",
+        "ERROR_NOT_ACCESSIBLE"    : "L'accés a aquest compte actualment no està permès. Torneu-ho a provar més endavant.",
+
+        "INFO_PASSWORD_EXPIRED" : "La vostra contrasenya ha caducat i s'ha de restablir. Introduïu una nova contrasenya per continuar.",
+
+        "FIELD_HEADER_NEW_PASSWORD"         : "Nova contrasenya",
+        "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Confirmar nova contrasenya"
+
+    },
+
+    "CONNECTION_ATTRIBUTES" : {
+
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "Nombre màxim de connexions:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Nombre màxim de connexions per usuari:",
+
+        "FIELD_HEADER_FAILOVER_ONLY"            : "Ús només per fer-ho desactivar:",
+        "FIELD_HEADER_WEIGHT"                   : "Pes de la connexió:",
+
+        "FIELD_HEADER_GUACD_HOSTNAME"   : "Nom de l'amfitrió:",
+        "FIELD_HEADER_GUACD_ENCRYPTION" : "Xifrat:",
+        "FIELD_HEADER_GUACD_PORT"       : "Port:",
+
+        "FIELD_OPTION_GUACD_ENCRYPTION_EMPTY" : "",
+        "FIELD_OPTION_GUACD_ENCRYPTION_NONE"  : "Cap (sense xifrar)",
+        "FIELD_OPTION_GUACD_ENCRYPTION_SSL"   : "SSL / TLS",
+
+        "SECTION_HEADER_CONCURRENCY"    : "Límits de concurrència",
+        "SECTION_HEADER_LOAD_BALANCING" : "Equilibri de càrrega",
+        "SECTION_HEADER_GUACD"          : "Paràmetres del proxy de Guacamole (guacd)"
+
+    },
+
+    "CONNECTION_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_ENABLE_SESSION_AFFINITY"  : "Activa l’afinitat de la sessió:",
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "Nombre màxim de connexions:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Nombre màxim de connexions per usuari:",
+
+        "SECTION_HEADER_CONCURRENCY" : "Límits de concurrència (grups d'equilibri)"
+
+    },
+
+    "DATA_SOURCE_MYSQL" : {
+        "NAME" : "MySQL"
+    },
+
+    "DATA_SOURCE_MYSQL_SHARED" : {
+        "NAME" : "Connexions compartides (MySQL)"
+    },
+
+    "DATA_SOURCE_POSTGRESQL" : {
+        "NAME" : "PostgreSQL"
+    },
+
+    "DATA_SOURCE_POSTGRESQL_SHARED" : {
+        "NAME" : "Connexions compartides (PostgreSQL)"
+    },
+
+    "DATA_SOURCE_SQLSERVER" : {
+        "NAME" : "SQL Server"
+    },
+
+    "DATA_SOURCE_SQLSERVER_SHARED" : {
+        "NAME" : "Connexions compartides (SQL Server)"
+    },
+
+    "HOME" : {
+        "INFO_SHARED_BY" : "Compartit per {USERNAME}"
+    },
+
+    "PASSWORD_POLICY" : {
+
+        "ERROR_CONTAINS_USERNAME"      : "Les contrasenyes poden no contenir el nom d'usuari.",
+        "ERROR_REQUIRES_DIGIT"         : "Les contrasenyes han de contenir almenys un dígit.",
+        "ERROR_REQUIRES_MULTIPLE_CASE" : "Les contrasenyes han de contenir majúscules i minúscules.",
+        "ERROR_REQUIRES_NON_ALNUM"     : "Les contrasenyes han de contenir almenys un símbol.",
+        "ERROR_REUSED"                 : "Aquesta contrasenya ja s'ha utilitzat. No reutilitzeu cap de l'anterior {HISTORY_SIZE} {HISTORY_SIZE, plural, one{password} other{passwords}}.",
+        "ERROR_TOO_SHORT"              : "Les contrasenyes han de ser com a mínim {LENGTH} {LENGTH, plural, one{character} other{characters}} de llarg.",
+        "ERROR_TOO_YOUNG"              : "La contrasenya d’aquest compte ja s’ha restablert. Espereu com a mínim {WAIT} more {WAIT, plural, one{day} other{days}} abans de canviar la contrasenya de nou."
+
+    },
+
+    "USER_ATTRIBUTES" : {
+
+        "FIELD_HEADER_DISABLED"            : "Inici de sessió desactivat:",
+        "FIELD_HEADER_EXPIRED"             : "Contrasenya caducada:",
+        "FIELD_HEADER_ACCESS_WINDOW_END"   : "No permetre l'accés després:",
+        "FIELD_HEADER_ACCESS_WINDOW_START" : "Permet l'accés després:",
+        "FIELD_HEADER_TIMEZONE"            : "Zona horària de l'usuari:",
+        "FIELD_HEADER_VALID_FROM"          : "Activa el compte després de",
+        "FIELD_HEADER_VALID_UNTIL"         : "Desactiva el compte després de",
+
+        "SECTION_HEADER_RESTRICTIONS" : "Restriccions del compte",
+        "SECTION_HEADER_PROFILE"      : "Perfil"
+
+    },
+
+    "USER_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_DISABLED" : "Desactivat:",
+
+        "SECTION_HEADER_RESTRICTIONS" : "Restriccions de grup"
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
index 1cecf80..46a3f36 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.2.0</version>
+        <version>1.3.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -99,21 +99,21 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-mysql</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
         </dependency>
 
         <!-- PostgreSQL Authentication Extension -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-postgresql</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
         </dependency>
 
         <!-- SQL Server Authentication Extension -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-sqlserver</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
index 94fb32f..1943d33 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.2.0</version>
+        <version>1.3.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json
index f6acceb..6e9d72d 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "MySQL Authentication",
     "namespace" : "guac-mysql",
@@ -19,6 +19,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/es.json",
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
index 391e90d..d42b47a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
@@ -63,17 +63,38 @@
         FROM guacamole_connection
     </select>
 
-    <!-- Select identifiers of all readable connections -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT connection_id
+    <!--
+      * SQL fragment which lists the IDs of all connections readable by the
+      * entity having the given entity ID. If group identifiers are provided,
+      * the IDs of the entities for all groups having those identifiers are
+      * tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT connection_id
         FROM guacamole_connection_permission
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable connections -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select all connection identifiers within a particular connection group -->
@@ -89,16 +110,15 @@
     <select id="selectReadableIdentifiersWithin" resultType="string">
         SELECT guacamole_connection.connection_id
         FROM guacamole_connection
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
         WHERE
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=VARCHAR}</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
     </select>
 
     <!-- Select multiple connections by identifier -->
@@ -166,53 +186,50 @@
             failover_only,
             MAX(start_date) AS last_active
         FROM guacamole_connection
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
         LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         WHERE guacamole_connection.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_connection_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND guacamole_connection.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
         GROUP BY guacamole_connection.connection_id;
 
         SELECT primary_connection_id, guacamole_sharing_profile.sharing_profile_id
         FROM guacamole_sharing_profile
-        JOIN guacamole_sharing_profile_permission ON guacamole_sharing_profile_permission.sharing_profile_id = guacamole_sharing_profile.sharing_profile_id
         WHERE primary_connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_sharing_profile.sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_connection_attribute.connection_id,
             attribute_name,
             attribute_value
         FROM guacamole_connection_attribute
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection_attribute.connection_id
         WHERE guacamole_connection_attribute.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_attribute.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
index d74d4c4..022ec97 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
@@ -108,28 +108,35 @@
         LEFT JOIN guacamole_user       ON guacamole_connection_history.user_id       = guacamole_user.user_id
 
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
+            
+            <if test="identifier != null">
+                guacamole_connection_history.connection_id = #{identifier,jdbcType=VARCHAR}
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
 
-                guacamole_connection_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                    guacamole_connection_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                    )
+
+                    OR guacamole_connection_history.connection_id IN (
+                        SELECT connection_id
+                        FROM guacamole_connection
+                        WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
                 )
-
-                OR guacamole_connection_history.connection_id IN (
-                    SELECT connection_id
-                    FROM guacamole_connection
-                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
-            )
-        </foreach>
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
@@ -165,52 +172,55 @@
         LEFT JOIN guacamole_connection ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         LEFT JOIN guacamole_user       ON guacamole_connection_history.user_id       = guacamole_user.user_id
 
-        <!-- Restrict to readable connections -->
-        JOIN guacamole_connection_permission ON
-                guacamole_connection_history.connection_id = guacamole_connection_permission.connection_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_connection_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND guacamole_connection_permission.permission = 'READ'
-
-        <!-- Restrict to readable users -->
-        JOIN guacamole_user_permission ON
-                guacamole_connection_history.user_id = guacamole_user_permission.affected_user_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND guacamole_user_permission.permission = 'READ'
-
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
-
-                guacamole_connection_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-                    WHERE
-                            POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
-                        AND guacamole_entity.type = 'USER'
-                )
-
-                OR guacamole_connection_history.connection_id IN (
-                    SELECT connection_id
-                    FROM guacamole_connection
-                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
+        <where>
+            
+            <!-- Restrict to readable connections -->
+            guacamole_connection_history.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
             )
-        </foreach>
+
+            <!-- Restrict to readable users -->
+            AND guacamole_connection_history.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
+
+            <if test="identifier != null">
+                AND guacamole_connection_history.connection_id = #{identifier,jdbcType=VARCHAR}
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
+
+                    guacamole_connection_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
+                        WHERE
+                                POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
+                            AND guacamole_entity.type = 'USER'
+                    )
+
+                    OR guacamole_connection_history.connection_id IN (
+                        SELECT connection_id
+                        FROM guacamole_connection
+                        WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
+                )
+            </foreach>
+        
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
index 9addd3c..7274f79 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
@@ -64,17 +64,38 @@
         FROM guacamole_connection_group
     </select>
 
-    <!-- Select identifiers of all readable connection groups -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT connection_group_id
+    <!--
+      * SQL fragment which lists the IDs of all connection groups readable by
+      * the entity having the given entity ID. If group identifiers are
+      * provided, the IDs of the entities for all groups having those
+      * identifiers are tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT connection_group_id
         FROM guacamole_connection_group_permission
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable connection groups -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select all connection identifiers within a particular connection group -->
@@ -90,16 +111,15 @@
     <select id="selectReadableIdentifiersWithin" resultType="string">
         SELECT guacamole_connection_group.connection_group_id
         FROM guacamole_connection_group
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
         WHERE
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=VARCHAR}</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
     </select>
 
     <!-- Select multiple connection groups by identifier -->
@@ -163,66 +183,62 @@
             max_connections_per_user,
             enable_session_affinity
         FROM guacamole_connection_group
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
         WHERE guacamole_connection_group.connection_group_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_group.connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT parent_id, guacamole_connection_group.connection_group_id
         FROM guacamole_connection_group
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
         WHERE parent_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_group.connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT parent_id, guacamole_connection.connection_id
         FROM guacamole_connection
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
         WHERE parent_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_connection_group_attribute.connection_group_id,
             attribute_name,
             attribute_value
         FROM guacamole_connection_group_attribute
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group_attribute.connection_group_id
         WHERE guacamole_connection_group_attribute.connection_group_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_group_attribute.connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
index 455f31f..ad8076c 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="ConnectionGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_group_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
index 862c5c7..406490f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="ConnectionPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
index bf8706e..072e644 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="SharingProfilePermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             sharing_profile_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
index ea76617..dab3804 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="UserGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
index 52c83e3..58fba14 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="UserPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
index 7ffdc3d..eb80c1b 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
@@ -47,17 +47,38 @@
         FROM guacamole_sharing_profile
     </select>
 
-    <!-- Select identifiers of all readable sharing profiles -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT sharing_profile_id
+    <!--
+      * SQL fragment which lists the IDs of all sharing profiles readable by
+      * the entity having the given entity ID. If group identifiers are
+      * provided, the IDs of the entities for all groups having those
+      * identifiers are tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT sharing_profile_id
         FROM guacamole_sharing_profile_permission
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable sharing profiles -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select multiple sharing profiles by identifier -->
@@ -97,36 +118,34 @@
             guacamole_sharing_profile.sharing_profile_name,
             primary_connection_id
         FROM guacamole_sharing_profile
-        JOIN guacamole_sharing_profile_permission ON guacamole_sharing_profile_permission.sharing_profile_id = guacamole_sharing_profile.sharing_profile_id
         WHERE guacamole_sharing_profile.sharing_profile_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_sharing_profile.sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_sharing_profile_attribute.sharing_profile_id,
             attribute_name,
             attribute_value
         FROM guacamole_sharing_profile_attribute
-        JOIN guacamole_sharing_profile_permission ON guacamole_sharing_profile_permission.sharing_profile_id = guacamole_sharing_profile_attribute.sharing_profile_id
         WHERE guacamole_sharing_profile_attribute.sharing_profile_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_sharing_profile_attribute.sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
index a27ff1b..0dcfa2e 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
@@ -63,20 +63,45 @@
         WHERE guacamole_entity.type = 'USER'
     </select>
 
+    <!--
+      * SQL fragment which lists the IDs of all users readable by the entity
+      * having the given entity ID. If group identifiers are provided, the IDs
+      * of the entities for all groups having those identifiers are tested, as
+      * well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT guacamole_user_permission.affected_user_id
+        FROM guacamole_user_permission
+        WHERE
+            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
+                <property name="column"   value="entity_id"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
+            </include>
+            AND permission = 'READ'
+    </sql>
+
     <!-- Select usernames of all readable users -->
     <select id="selectReadableIdentifiers" resultType="string">
         SELECT guacamole_entity.name
         FROM guacamole_user
         JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_entity.type = 'USER'
-            AND permission = 'READ'
     </select>
 
     <!-- Select multiple users by username -->
@@ -154,7 +179,6 @@
             MAX(start_date) AS last_active
         FROM guacamole_user
         JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
@@ -162,12 +186,12 @@
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
         GROUP BY guacamole_user.user_id, guacamole_entity.entity_id;
 
         SELECT
@@ -177,19 +201,18 @@
         FROM guacamole_user_attribute
         JOIN guacamole_user ON guacamole_user.user_id = guacamole_user_attribute.user_id
         JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
index 1b0ec4e..764213e 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
@@ -40,16 +40,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_user_group ON guacamole_user_group_member.user_group_id = guacamole_user_group.user_group_id
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.member_entity_id = #{parent.entityID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete parent groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
index d9c02ef..447321a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
@@ -105,25 +105,32 @@
         FROM guacamole_user_history
 
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
+            
+            <if test="username != null">
+                guacamole_user_history.username = #{username,jdbcType=VARCHAR}
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
 
-                guacamole_user_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-                    WHERE
-                            POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
-                        AND guacamole_entity.type = 'USER'),
+                    guacamole_user_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
+                        WHERE
+                                POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
+                            AND guacamole_entity.type = 'USER'),
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
                 )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
-            )
-        </foreach>
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
@@ -153,36 +160,41 @@
             guacamole_user_history.end_date
         FROM guacamole_user_history
 
-        <!-- Restrict to readable users -->
-        JOIN guacamole_user_permission ON
-                guacamole_user_history.user_id       = guacamole_user_permission.affected_user_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND guacamole_user_permission.permission = 'READ'
-
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
 
-                guacamole_user_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-                    WHERE
-                            POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
-                        AND guacamole_entity.type = 'USER'
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
+            <!-- Restrict to readable users -->
+            guacamole_connection_history.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
             )
-        </foreach>
+            
+            <if test="username != null">
+                AND guacamole_entity.name = #{username,jdbcType=VARCHAR}
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
+
+                    guacamole_user_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
+                        WHERE
+                                POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
+                            AND guacamole_entity.type = 'USER'
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
+                )
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
index 37092b4..4d68da7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
@@ -49,20 +49,45 @@
         WHERE guacamole_entity.type = 'USER_GROUP'
     </select>
 
+    <!--
+      * SQL fragment which lists the IDs of all user groups readable by the
+      * entity having the given entity ID. If group identifiers are provided,
+      * the IDs of the entities for all groups having those identifiers are
+      * tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT guacamole_user_group_permission.affected_user_group_id
+        FROM guacamole_user_group_permission
+        WHERE
+            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
+                <property name="column"   value="entity_id"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
+            </include>
+            AND permission = 'READ'
+    </sql>
+
     <!-- Select names of all readable groups -->
     <select id="selectReadableIdentifiers" resultType="string">
         SELECT guacamole_entity.name
         FROM guacamole_user_group
         JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_entity.type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Select multiple groups by name -->
@@ -110,19 +135,18 @@
             disabled
         FROM guacamole_user_group
         JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER_GROUP'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_user_group_attribute.user_group_id,
@@ -131,19 +155,18 @@
         FROM guacamole_user_group_attribute
         JOIN guacamole_user_group ON guacamole_user_group.user_group_id = guacamole_user_group_attribute.user_group_id
         JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER_GROUP'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND  guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
index aedc956..bfcd6c6 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
@@ -39,16 +39,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group_member.member_entity_id
         JOIN guacamole_user_group ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.user_group_id = #{parent.objectID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete member groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
index 9e08203..609d907 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
@@ -39,16 +39,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group_member.member_entity_id
         JOIN guacamole_user ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.user_group_id = #{parent.objectID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete member users by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
index 4ef3c72..9fa81b9 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
@@ -40,16 +40,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_user_group ON guacamole_user_group_member.user_group_id = guacamole_user_group.user_group_id
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.member_entity_id = #{parent.entityID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete parent groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
index 2aaab92..7717404 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.2.0</version>
+        <version>1.3.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
index 0425b45..707bbe5 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "PostgreSQL Authentication",
     "namespace" : "guac-postgresql",
@@ -19,6 +19,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/es.json",
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
index 859cec5..05c4f61 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
@@ -63,17 +63,38 @@
         FROM guacamole_connection
     </select>
 
-    <!-- Select identifiers of all readable connections -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT connection_id
+    <!--
+      * SQL fragment which lists the IDs of all connections readable by the
+      * entity having the given entity ID. If group identifiers are provided,
+      * the IDs of the entities for all groups having those identifiers are
+      * tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT connection_id
         FROM guacamole_connection_permission
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable connections -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select all connection identifiers within a particular connection group -->
@@ -89,16 +110,15 @@
     <select id="selectReadableIdentifiersWithin" resultType="string">
         SELECT guacamole_connection.connection_id
         FROM guacamole_connection
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
         WHERE
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=INTEGER}::integer</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
     </select>
 
     <!-- Select multiple connections by identifier -->
@@ -166,53 +186,50 @@
             failover_only,
             MAX(start_date) AS last_active
         FROM guacamole_connection
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
         LEFT JOIN guacamole_connection_history ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         WHERE guacamole_connection.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_connection_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND guacamole_connection.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
         GROUP BY guacamole_connection.connection_id;
 
         SELECT primary_connection_id, guacamole_sharing_profile.sharing_profile_id
         FROM guacamole_sharing_profile
-        JOIN guacamole_sharing_profile_permission ON guacamole_sharing_profile_permission.sharing_profile_id = guacamole_sharing_profile.sharing_profile_id
         WHERE primary_connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_sharing_profile.sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_connection_attribute.connection_id,
             attribute_name,
             attribute_value
         FROM guacamole_connection_attribute
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection_attribute.connection_id
         WHERE guacamole_connection_attribute.connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_attribute.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
index e8e8876..1dcf950 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
@@ -106,28 +106,35 @@
         FROM guacamole_connection_history
 
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
+            
+            <if test="identifier != null">
+                guacamole_connection_history.connection_id = #{identifier,jdbcType=INTEGER}::integer
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
 
-                guacamole_connection_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                    guacamole_connection_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN username) > 0
+                    )
+
+                    OR guacamole_connection_history.connection_id IN (
+                        SELECT connection_id
+                        FROM guacamole_connection
+                        WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
                 )
-
-                OR guacamole_connection_history.connection_id IN (
-                    SELECT connection_id
-                    FROM guacamole_connection
-                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
-            )
-        </foreach>
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
@@ -163,52 +170,55 @@
         LEFT JOIN guacamole_connection            ON guacamole_connection_history.connection_id = guacamole_connection.connection_id
         LEFT JOIN guacamole_user                  ON guacamole_connection_history.user_id       = guacamole_user.user_id
 
-        <!-- Restrict to readable connections -->
-        JOIN guacamole_connection_permission ON
-                guacamole_connection_history.connection_id = guacamole_connection_permission.connection_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_connection_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND guacamole_connection_permission.permission = 'READ'
-
-        <!-- Restrict to readable users -->
-        JOIN guacamole_user_permission ON
-                guacamole_connection_history.user_id = guacamole_user_permission.affected_user_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND guacamole_user_permission.permission = 'READ'
-
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
-
-                guacamole_connection_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-                    WHERE
-                            POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
-                        AND guacamole_entity.type = 'USER'::guacamole_entity_type
-                )
-
-                OR guacamole_connection_history.connection_id IN (
-                    SELECT connection_id
-                    FROM guacamole_connection
-                    WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
+        <where>
+            
+            <!-- Restrict to readable connections -->
+            guacamole_connection_history.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
             )
-        </foreach>
+
+            <!-- Restrict to readable users -->
+            AND guacamole_connection_history.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
+
+            <if test="identifier != null">
+                AND guacamole_connection_history.connection_id = #{identifier,jdbcType=INTEGER}::integer
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
+
+                    guacamole_connection_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
+                        WHERE
+                                POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
+                            AND guacamole_entity.type = 'USER'::guacamole_entity_type
+                    )
+
+                    OR guacamole_connection_history.connection_id IN (
+                        SELECT connection_id
+                        FROM guacamole_connection
+                        WHERE POSITION(#{term.term,jdbcType=VARCHAR} IN connection_name) > 0
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
+                )
+            </foreach>
+
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
index 37841de..dd2dbab 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
@@ -64,17 +64,38 @@
         FROM guacamole_connection_group
     </select>
 
-    <!-- Select identifiers of all readable connection groups -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT connection_group_id
+    <!--
+      * SQL fragment which lists the IDs of all connection groups readable by
+      * the entity having the given entity ID. If group identifiers are
+      * provided, the IDs of the entities for all groups having those
+      * identifiers are tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT connection_group_id
         FROM guacamole_connection_group_permission
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable connection groups -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select all connection identifiers within a particular connection group -->
@@ -90,16 +111,15 @@
     <select id="selectReadableIdentifiersWithin" resultType="string">
         SELECT guacamole_connection_group.connection_group_id
         FROM guacamole_connection_group
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
         WHERE
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=INTEGER}::integer</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
     </select>
 
     <!-- Select multiple connection groups by identifier -->
@@ -163,66 +183,62 @@
             max_connections_per_user,
             enable_session_affinity
         FROM guacamole_connection_group
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
         WHERE guacamole_connection_group.connection_group_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_group.connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT parent_id, guacamole_connection_group.connection_group_id
         FROM guacamole_connection_group
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group.connection_group_id
         WHERE parent_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_group.connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT parent_id, guacamole_connection.connection_id
         FROM guacamole_connection
-        JOIN guacamole_connection_permission ON guacamole_connection_permission.connection_id = guacamole_connection.connection_id
         WHERE parent_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection.connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_connection_group_attribute.connection_group_id,
             attribute_name,
             attribute_value
         FROM guacamole_connection_group_attribute
-        JOIN guacamole_connection_group_permission ON guacamole_connection_group_permission.connection_group_id = guacamole_connection_group_attribute.connection_group_id
         WHERE guacamole_connection_group_attribute.connection_group_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_connection_group_attribute.connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
index e3b4053..b67f3b4 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="ConnectionGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_group_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
index cd9cf11..f63fbae 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="ConnectionPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
index d4ce589..35e04a7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="SharingProfilePermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             sharing_profile_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
index afa1035..fd86d9d 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="UserGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
index fbd8ae0..a4b5f1b 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="UserPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
index 62548d7..71fec72 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
@@ -47,17 +47,38 @@
         FROM guacamole_sharing_profile
     </select>
 
-    <!-- Select identifiers of all readable sharing profiles -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT sharing_profile_id
+    <!--
+      * SQL fragment which lists the IDs of all sharing profiles readable by
+      * the entity having the given entity ID. If group identifiers are
+      * provided, the IDs of the entities for all groups having those
+      * identifiers are tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT sharing_profile_id
         FROM guacamole_sharing_profile_permission
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable sharing profiles -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select multiple sharing profiles by identifier -->
@@ -97,36 +118,34 @@
             guacamole_sharing_profile.sharing_profile_name,
             primary_connection_id
         FROM guacamole_sharing_profile
-        JOIN guacamole_sharing_profile_permission ON guacamole_sharing_profile_permission.sharing_profile_id = guacamole_sharing_profile.sharing_profile_id
         WHERE guacamole_sharing_profile.sharing_profile_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_sharing_profile.sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_sharing_profile_attribute.sharing_profile_id,
             attribute_name,
             attribute_value
         FROM guacamole_sharing_profile_attribute
-        JOIN guacamole_sharing_profile_permission ON guacamole_sharing_profile_permission.sharing_profile_id = guacamole_sharing_profile_attribute.sharing_profile_id
         WHERE guacamole_sharing_profile_attribute.sharing_profile_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}::integer
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_sharing_profile_attribute.sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
index 1181b37..94d607e 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
@@ -63,20 +63,45 @@
         WHERE guacamole_entity.type = 'USER'::guacamole_entity_type
     </select>
 
+    <!--
+      * SQL fragment which lists the IDs of all users readable by the entity
+      * having the given entity ID. If group identifiers are provided, the IDs
+      * of the entities for all groups having those identifiers are tested, as
+      * well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT guacamole_user_permission.affected_user_id
+        FROM guacamole_user_permission
+        WHERE
+            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
+                <property name="column"   value="entity_id"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
+            </include>
+            AND permission = 'READ'
+    </sql>
+
     <!-- Select usernames of all readable users -->
     <select id="selectReadableIdentifiers" resultType="string">
         SELECT guacamole_entity.name
         FROM guacamole_user
         JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_entity.type = 'USER'::guacamole_entity_type
-            AND permission = 'READ'
     </select>
 
     <!-- Select multiple users by username -->
@@ -154,7 +179,6 @@
             MAX(start_date) AS last_active
         FROM guacamole_user
         JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         LEFT JOIN guacamole_user_history ON guacamole_user_history.user_id = guacamole_user.user_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
@@ -162,12 +186,12 @@
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER'::guacamole_entity_type
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
         GROUP BY guacamole_user.user_id, guacamole_entity.entity_id;
 
         SELECT
@@ -177,19 +201,18 @@
         FROM guacamole_user_attribute
         JOIN guacamole_user ON guacamole_user.user_id = guacamole_user_attribute.user_id
         JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER'::guacamole_entity_type
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
index bcff7a2..ef7dc42 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
@@ -40,16 +40,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_user_group ON guacamole_user_group_member.user_group_id = guacamole_user_group.user_group_id
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.member_entity_id = #{parent.entityID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER_GROUP'::guacamole_entity_type
-            AND permission = 'READ'
     </select>
 
     <!-- Delete parent groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
index 6311a25..204329f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
@@ -105,25 +105,32 @@
         FROM guacamole_user_history
 
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
+            
+            <if test="username != null">
+                guacamole_user_history.username = #{username,jdbcType=VARCHAR}
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
 
-                guacamole_user_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-                    WHERE
-                            POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
-                        AND guacamole_entity.type = 'USER'::guacamole_entity_type),
+                    guacamole_user_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
+                        WHERE
+                                POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
+                            AND guacamole_entity.type = 'USER'::guacamole_entity_type),
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
                 )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
-            )
-        </foreach>
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
@@ -153,36 +160,41 @@
             guacamole_user_history.end_date
         FROM guacamole_user_history
 
-        <!-- Restrict to readable users -->
-        JOIN guacamole_user_permission ON
-                guacamole_user_history.user_id       = guacamole_user_permission.affected_user_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND guacamole_user_permission.permission = 'READ'
-
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
 
-                guacamole_user_history.user_id IN (
-                    SELECT user_id
-                    FROM guacamole_user
-                    JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
-                    WHERE
-                            POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
-                        AND guacamole_entity.type = 'USER'::guacamole_entity_type
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
+            <!-- Restrict to readable users -->
+            guacamole_connection_history.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
             )
-        </foreach>
+
+            <if test="username != null">
+                AND guacamole_entity.name = #{username,jdbcType=VARCHAR}
+            </if>
+
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
+
+                    guacamole_user_history.user_id IN (
+                        SELECT user_id
+                        FROM guacamole_user
+                        JOIN guacamole_entity ON guacamole_user.entity_id = guacamole_entity.entity_id
+                        WHERE
+                                POSITION(#{term.term,jdbcType=VARCHAR} IN guacamole_entity.name) > 0
+                            AND guacamole_entity.type = 'USER'::guacamole_entity_type
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
+                )
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
index 0006d42..88232ad 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
@@ -49,20 +49,45 @@
         WHERE guacamole_entity.type = 'USER_GROUP'::guacamole_entity_type
     </select>
 
+    <!--
+      * SQL fragment which lists the IDs of all user groups readable by the
+      * entity having the given entity ID. If group identifiers are provided,
+      * the IDs of the entities for all groups having those identifiers are
+      * tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT guacamole_user_group_permission.affected_user_group_id
+        FROM guacamole_user_group_permission
+        WHERE
+            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
+                <property name="column"   value="entity_id"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
+            </include>
+            AND permission = 'READ'
+    </sql>
+
     <!-- Select names of all readable groups -->
     <select id="selectReadableIdentifiers" resultType="string">
         SELECT guacamole_entity.name
         FROM guacamole_user_group
         JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_entity.type = 'USER_GROUP'::guacamole_entity_type
-            AND permission = 'READ'
     </select>
 
     <!-- Select multiple groups by name -->
@@ -110,19 +135,18 @@
             disabled
         FROM guacamole_user_group
         JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER_GROUP'::guacamole_entity_type
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             guacamole_user_group_attribute.user_group_id,
@@ -131,19 +155,18 @@
         FROM guacamole_user_group_attribute
         JOIN guacamole_user_group ON guacamole_user_group.user_group_id = guacamole_user_group_attribute.user_group_id
         JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE guacamole_entity.name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND guacamole_entity.type = 'USER_GROUP'::guacamole_entity_type
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND  guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
index 13f4d71..09f12b2 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
@@ -39,16 +39,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group_member.member_entity_id
         JOIN guacamole_user_group ON guacamole_user_group.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.user_group_id = #{parent.objectID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER_GROUP'::guacamole_entity_type
-            AND permission = 'READ'
     </select>
 
     <!-- Delete member groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
index 562b1ad..a3ad5a7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
@@ -39,16 +39,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group_member.member_entity_id
         JOIN guacamole_user ON guacamole_user.entity_id = guacamole_entity.entity_id
-        JOIN guacamole_user_permission ON affected_user_id = guacamole_user.user_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user.user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.user_group_id = #{parent.objectID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER'::guacamole_entity_type
-            AND permission = 'READ'
     </select>
 
     <!-- Delete member users by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
index 035211c..9fec628 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
@@ -40,16 +40,15 @@
         FROM guacamole_user_group_member
         JOIN guacamole_user_group ON guacamole_user_group_member.user_group_id = guacamole_user_group.user_group_id
         JOIN guacamole_entity ON guacamole_entity.entity_id = guacamole_user_group.entity_id
-        JOIN guacamole_user_group_permission ON affected_user_group_id = guacamole_user_group.user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="guacamole_user_group_permission.entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            guacamole_user_group.user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND guacamole_user_group_member.member_entity_id = #{parent.entityID,jdbcType=INTEGER}
             AND guacamole_entity.type = 'USER_GROUP'::guacamole_entity_type
-            AND permission = 'READ'
     </select>
 
     <!-- Delete parent groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
index f750466..54b6463 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.2.0</version>
+        <version>1.3.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json
index a0fe6ba..17fc577 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "SQLServer Authentication",
     "namespace" : "guac-sqlserver",
@@ -19,6 +19,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/es.json",
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
index 54cb575..7b1adae 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionMapper.xml
@@ -63,17 +63,38 @@
         FROM [guacamole_connection]
     </select>
 
-    <!-- Select identifiers of all readable connections -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT connection_id
+    <!--
+      * SQL fragment which lists the IDs of all connections readable by the
+      * entity having the given entity ID. If group identifiers are provided,
+      * the IDs of the entities for all groups having those identifiers are
+      * tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT connection_id
         FROM [guacamole_connection_permission]
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable connections -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select all connection identifiers within a particular connection group -->
@@ -89,16 +110,15 @@
     <select id="selectReadableIdentifiersWithin" resultType="string">
         SELECT [guacamole_connection].connection_id
         FROM [guacamole_connection]
-        JOIN [guacamole_connection_permission] ON [guacamole_connection_permission].connection_id = [guacamole_connection].connection_id
         WHERE
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=INTEGER}</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
     </select>
 
     <!-- Select multiple connections by identifier -->
@@ -172,51 +192,48 @@
                 WHERE [guacamole_connection_history].connection_id = [guacamole_connection].connection_id
             ) AS last_active
         FROM [guacamole_connection]
-        JOIN [guacamole_connection_permission] ON [guacamole_connection_permission].connection_id = [guacamole_connection].connection_id
         WHERE [guacamole_connection].connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_connection_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_connection].connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT primary_connection_id, [guacamole_sharing_profile].sharing_profile_id
         FROM [guacamole_sharing_profile]
-        JOIN [guacamole_sharing_profile_permission] ON [guacamole_sharing_profile_permission].sharing_profile_id = [guacamole_sharing_profile].sharing_profile_id
         WHERE primary_connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_sharing_profile].sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             [guacamole_connection_attribute].connection_id,
             attribute_name,
             attribute_value
         FROM [guacamole_connection_attribute]
-        JOIN [guacamole_connection_permission] ON [guacamole_connection_permission].connection_id = [guacamole_connection_attribute].connection_id
         WHERE [guacamole_connection_attribute].connection_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_connection_attribute].connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
index 5d97b0b..0ea14cc 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connection/ConnectionRecordMapper.xml
@@ -106,28 +106,35 @@
         FROM [guacamole_connection_history]
 
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
+            
+            <if test="identifier != null">
+                [guacamole_connection_history].connection_id = #{identifier,jdbcType=INTEGER}
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
 
-                [guacamole_connection_history].user_id IN (
-                    SELECT user_id
-                    FROM [guacamole_user]
-                    WHERE CHARINDEX(#{term.term,jdbcType=VARCHAR}, username) > 0
+                    [guacamole_connection_history].user_id IN (
+                        SELECT user_id
+                        FROM [guacamole_user]
+                        WHERE CHARINDEX(#{term.term,jdbcType=VARCHAR}, username) > 0
+                    )
+
+                    OR [guacamole_connection_history].connection_id IN (
+                        SELECT connection_id
+                        FROM [guacamole_connection]
+                        WHERE CHARINDEX(#{term.term,jdbcType=VARCHAR}, connection_name) > 0
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
                 )
-
-                OR [guacamole_connection_history].connection_id IN (
-                    SELECT connection_id
-                    FROM [guacamole_connection]
-                    WHERE CHARINDEX(#{term.term,jdbcType=VARCHAR}, connection_name) > 0
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
-            )
-        </foreach>
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
@@ -161,52 +168,55 @@
         LEFT JOIN [guacamole_connection]            ON [guacamole_connection_history].connection_id = [guacamole_connection].connection_id
         LEFT JOIN [guacamole_user]                  ON [guacamole_connection_history].user_id       = [guacamole_user].user_id
 
-        <!-- Restrict to readable connections -->
-        JOIN [guacamole_connection_permission] ON
-                [guacamole_connection_history].connection_id = [guacamole_connection_permission].connection_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_connection_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND [guacamole_connection_permission].permission = 'READ'
-
-        <!-- Restrict to readable users -->
-        JOIN [guacamole_user_permission] ON
-                [guacamole_connection_history].user_id = [guacamole_user_permission].affected_user_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND [guacamole_user_permission].permission = 'READ'
-
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
-
-                [guacamole_connection_history].user_id IN (
-                    SELECT user_id
-                    FROM [guacamole_user]
-                    JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
-                    WHERE
-                            CHARINDEX(#{term.term,jdbcType=VARCHAR}, [guacamole_entity].name) > 0
-                        AND [guacamole_entity].type = 'USER'
-                )
-
-                OR [guacamole_connection_history].connection_id IN (
-                    SELECT connection_id
-                    FROM [guacamole_connection]
-                    WHERE CHARINDEX(#{term.term,jdbcType=VARCHAR}, connection_name) > 0
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
+        <where>
+            
+            <!-- Restrict to readable connections -->
+            [guacamole_connection_history].connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
             )
-        </foreach>
+
+            <!-- Restrict to readable users -->
+            AND [guacamole_connection_history].user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
+
+            <if test="identifier != null">
+                AND [guacamole_connection_history].connection_id = #{identifier,jdbcType=INTEGER}
+            </if>
+            
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
+
+                    [guacamole_connection_history].user_id IN (
+                        SELECT user_id
+                        FROM [guacamole_user]
+                        JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
+                        WHERE
+                                CHARINDEX(#{term.term,jdbcType=VARCHAR}, [guacamole_entity].name) > 0
+                            AND [guacamole_entity].type = 'USER'
+                    )
+
+                    OR [guacamole_connection_history].connection_id IN (
+                        SELECT connection_id
+                        FROM [guacamole_connection]
+                        WHERE CHARINDEX(#{term.term,jdbcType=VARCHAR}, connection_name) > 0
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
+                )
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
index 32c1d13..4bc8a27 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupMapper.xml
@@ -64,17 +64,38 @@
         FROM [guacamole_connection_group]
     </select>
 
-    <!-- Select identifiers of all readable connection groups -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT connection_group_id
+    <!--
+      * SQL fragment which lists the IDs of all connection groups readable by
+      * the entity having the given entity ID. If group identifiers are
+      * provided, the IDs of the entities for all groups having those
+      * identifiers are tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT connection_group_id
         FROM [guacamole_connection_group_permission]
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable connection groups -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select all connection identifiers within a particular connection group -->
@@ -90,16 +111,15 @@
     <select id="selectReadableIdentifiersWithin" resultType="string">
         SELECT [guacamole_connection_group].connection_group_id
         FROM [guacamole_connection_group]
-        JOIN [guacamole_connection_group_permission] ON [guacamole_connection_group_permission].connection_group_id = [guacamole_connection_group].connection_group_id
         WHERE
             <if test="parentIdentifier != null">parent_id = #{parentIdentifier,jdbcType=INTEGER}</if>
             <if test="parentIdentifier == null">parent_id IS NULL</if>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ'
+            AND connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
     </select>
 
     <!-- Select multiple connection groups by identifier -->
@@ -163,66 +183,62 @@
             max_connections_per_user,
             enable_session_affinity
         FROM [guacamole_connection_group]
-        JOIN [guacamole_connection_group_permission] ON [guacamole_connection_group_permission].connection_group_id = [guacamole_connection_group].connection_group_id
         WHERE [guacamole_connection_group].connection_group_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_connection_group].connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT parent_id, [guacamole_connection_group].connection_group_id
         FROM [guacamole_connection_group]
-        JOIN [guacamole_connection_group_permission] ON [guacamole_connection_group_permission].connection_group_id = [guacamole_connection_group].connection_group_id
         WHERE parent_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_connection_group].connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT parent_id, [guacamole_connection].connection_id
         FROM [guacamole_connection]
-        JOIN [guacamole_connection_permission] ON [guacamole_connection_permission].connection_id = [guacamole_connection].connection_id
         WHERE parent_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_connection].connection_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connection.ConnectionMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             [guacamole_connection_group_attribute].connection_group_id,
             attribute_name,
             attribute_value
         FROM [guacamole_connection_group_attribute]
-        JOIN [guacamole_connection_group_permission] ON [guacamole_connection_group_permission].connection_group_id = [guacamole_connection_group_attribute].connection_group_id
         WHERE [guacamole_connection_group_attribute].connection_group_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_connection_group_attribute].connection_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
index ef58daf..23cef25 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="ConnectionGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_group_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
index 882e6dd..31f504f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="ConnectionPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
index 23feeee..bb45e4b 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="SharingProfilePermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             sharing_profile_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
index 6dd0b5d..8c7ff27 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="UserGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
index 4503419..5c08804 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
@@ -34,7 +34,7 @@
     <!-- Select all permissions for a given entity -->
     <select id="select" resultMap="UserPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
index dc87f53..34d9b58 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileMapper.xml
@@ -47,17 +47,38 @@
         FROM [guacamole_sharing_profile]
     </select>
 
-    <!-- Select identifiers of all readable sharing profiles -->
-    <select id="selectReadableIdentifiers" resultType="string">
-        SELECT sharing_profile_id
+    <!--
+      * SQL fragment which lists the IDs of all sharing profiles readable by
+      * the entity having the given entity ID. If group identifiers are
+      * provided, the IDs of the entities for all groups having those
+      * identifiers are tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT sharing_profile_id
         FROM [guacamole_sharing_profile_permission]
         WHERE
             <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
                 <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
             </include>
             AND permission = 'READ'
+    </sql>
+
+    <!-- Select identifiers of all readable sharing profiles -->
+    <select id="selectReadableIdentifiers" resultType="string">
+        <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+            <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+            <property name="groups"   value="effectiveGroups"/>
+        </include>
     </select>
 
     <!-- Select multiple sharing profiles by identifier -->
@@ -97,36 +118,34 @@
             [guacamole_sharing_profile].sharing_profile_name,
             primary_connection_id
         FROM [guacamole_sharing_profile]
-        JOIN [guacamole_sharing_profile_permission] ON [guacamole_sharing_profile_permission].sharing_profile_id = [guacamole_sharing_profile].sharing_profile_id
         WHERE [guacamole_sharing_profile].sharing_profile_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_sharing_profile].sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             [guacamole_sharing_profile_attribute].sharing_profile_id,
             attribute_name,
             attribute_value
         FROM [guacamole_sharing_profile_attribute]
-        JOIN [guacamole_sharing_profile_permission] ON [guacamole_sharing_profile_permission].sharing_profile_id = [guacamole_sharing_profile_attribute].sharing_profile_id
         WHERE [guacamole_sharing_profile_attribute].sharing_profile_id IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=INTEGER}
             </foreach>
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_sharing_profile_attribute].sharing_profile_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
index 7d70950..8034496 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserMapper.xml
@@ -63,20 +63,45 @@
         WHERE [guacamole_entity].type = 'USER'
     </select>
 
+    <!--
+      * SQL fragment which lists the IDs of all users readable by the entity
+      * having the given entity ID. If group identifiers are provided, the IDs
+      * of the entities for all groups having those identifiers are tested, as
+      * well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT [guacamole_user_permission].affected_user_id
+        FROM [guacamole_user_permission]
+        WHERE
+            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
+                <property name="column"   value="entity_id"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
+            </include>
+            AND permission = 'READ'
+    </sql>
+
     <!-- Select usernames of all readable users -->
     <select id="selectReadableIdentifiers" resultType="string">
         SELECT [guacamole_entity].name
         FROM [guacamole_user]
         JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_permission] ON affected_user_id = [guacamole_user].user_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            [guacamole_user].user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND [guacamole_entity].type = 'USER'
-            AND permission = 'READ'
     </select>
 
     <!-- Select multiple users by username -->
@@ -160,19 +185,18 @@
             ) AS last_active
         FROM [guacamole_user]
         JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_permission] ON affected_user_id = [guacamole_user].user_id
         WHERE [guacamole_entity].name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND [guacamole_entity].type = 'USER'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_user].user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             [guacamole_user_attribute].user_id,
@@ -181,19 +205,18 @@
         FROM [guacamole_user_attribute]
         JOIN [guacamole_user] ON [guacamole_user].user_id = [guacamole_user_attribute].user_id
         JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_permission] ON affected_user_id = [guacamole_user].user_id
         WHERE [guacamole_entity].name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND [guacamole_entity].type = 'USER'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_user].user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
index e6eccba..ee67931 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserParentUserGroupMapper.xml
@@ -40,16 +40,15 @@
         FROM [guacamole_user_group_member]
         JOIN [guacamole_user_group] ON [guacamole_user_group_member].user_group_id = [guacamole_user_group].user_group_id
         JOIN [guacamole_entity] ON [guacamole_entity].entity_id = [guacamole_user_group].entity_id
-        JOIN [guacamole_user_group_permission] ON affected_user_group_id = [guacamole_user_group].user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_group_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            [guacamole_user_group].user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND [guacamole_user_group_member].member_entity_id = #{parent.entityID,jdbcType=INTEGER}
             AND [guacamole_entity].type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete parent groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
index 7cc5efa..8518e70 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/user/UserRecordMapper.xml
@@ -105,25 +105,32 @@
         FROM [guacamole_user_history]
 
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
+            
+            <if test="username != null">
+                [guacamole_user_history].username = #{username,jdbcType=VARCHAR}
+            </if>
 
-                [guacamole_user_history].user_id IN (
-                    SELECT user_id
-                    FROM [guacamole_user]
-                    JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
-                    WHERE
-                            CHARINDEX(#{term.term,jdbcType=VARCHAR}, [guacamole_entity].name) > 0
-                        AND [guacamole_entity].type = 'USER'),
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
+
+                    [guacamole_user_history].user_id IN (
+                        SELECT user_id
+                        FROM [guacamole_user]
+                        JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
+                        WHERE
+                                CHARINDEX(#{term.term,jdbcType=VARCHAR}, [guacamole_entity].name) > 0
+                            AND [guacamole_entity].type = 'USER'),
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
                 )
+            </foreach>
 
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
-            )
-        </foreach>
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
@@ -151,36 +158,41 @@
             [guacamole_user_history].end_date
         FROM [guacamole_user_history]
 
-        <!-- Restrict to readable users -->
-        JOIN [guacamole_user_permission] ON
-                [guacamole_user_history].user_id       = [guacamole_user_permission].affected_user_id
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND [guacamole_user_permission].permission = 'READ'
-
         <!-- Search terms -->
-        <foreach collection="terms" item="term"
-                 open="WHERE " separator=" AND ">
-            (
+        <where>
 
-                [guacamole_user_history].user_id IN (
-                    SELECT user_id
-                    FROM [guacamole_user]
-                    JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
-                    WHERE
-                            CHARINDEX(#{term.term,jdbcType=VARCHAR}, [guacamole_entity].name) > 0
-                        AND [guacamole_entity].type = 'USER'
-                )
-
-                <if test="term.startDate != null and term.endDate != null">
-                    OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
-                </if>
-
+            <!-- Restrict to readable users -->
+            [guacamole_connection_history].user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
             )
-        </foreach>
+            
+            <if test="username != null">
+                AND [guacamole_entity].name = #{username,jdbcType=VARCHAR}
+            </if>
+
+            <foreach collection="terms" item="term" open=" AND " separator=" AND ">
+                (
+
+                    [guacamole_user_history].user_id IN (
+                        SELECT user_id
+                        FROM [guacamole_user]
+                        JOIN [guacamole_entity] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
+                        WHERE
+                                CHARINDEX(#{term.term,jdbcType=VARCHAR}, [guacamole_entity].name) > 0
+                            AND [guacamole_entity].type = 'USER'
+                    )
+
+                    <if test="term.startDate != null and term.endDate != null">
+                        OR start_date BETWEEN #{term.startDate,jdbcType=TIMESTAMP} AND #{term.endDate,jdbcType=TIMESTAMP}
+                    </if>
+
+                )
+            </foreach>
+            
+        </where>
 
         <!-- Bind sort property enum values for sake of readability -->
         <bind name="START_DATE" value="@org.apache.guacamole.net.auth.ActivityRecordSet$SortableProperty@START_DATE"/>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
index aed0247..21c776a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMapper.xml
@@ -49,20 +49,45 @@
         WHERE [guacamole_entity].type = 'USER_GROUP'
     </select>
 
+    <!--
+      * SQL fragment which lists the IDs of all user groups readable by the
+      * entity having the given entity ID. If group identifiers are provided,
+      * the IDs of the entities for all groups having those identifiers are
+      * tested, as well. Disabled groups are ignored.
+      *
+      * @param entityID
+      *     The ID of the specific entity to test against.
+      *
+      * @param groups
+      *     A collection of group identifiers to additionally test against.
+      *     Though this functionality is optional, a collection must always be
+      *     given, even if that collection is empty.
+      -->
+    <sql id="getReadableIDs">
+        SELECT DISTINCT [guacamole_user_group_permission].affected_user_group_id
+        FROM [guacamole_user_group_permission]
+        WHERE
+            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
+                <property name="column"   value="entity_id"/>
+                <property name="entityID" value="${entityID}"/>
+                <property name="groups"   value="${groups}"/>
+            </include>
+            AND permission = 'READ'
+    </sql>
+
     <!-- Select names of all readable groups -->
     <select id="selectReadableIdentifiers" resultType="string">
         SELECT [guacamole_entity].name
         FROM [guacamole_user_group]
         JOIN [guacamole_entity] ON [guacamole_user_group].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_group_permission] ON affected_user_group_id = [guacamole_user_group].user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_group_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            [guacamole_user_group].user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND [guacamole_entity].type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Select multiple groups by name -->
@@ -110,19 +135,18 @@
             disabled
         FROM [guacamole_user_group]
         JOIN [guacamole_entity] ON [guacamole_user_group].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_group_permission] ON affected_user_group_id = [guacamole_user_group].user_group_id
         WHERE [guacamole_entity].name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND [guacamole_entity].type = 'USER_GROUP'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_group_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND [guacamole_user_group].user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
         SELECT
             [guacamole_user_group_attribute].user_group_id,
@@ -131,19 +155,18 @@
         FROM [guacamole_user_group_attribute]
         JOIN [guacamole_user_group] ON [guacamole_user_group].user_group_id = [guacamole_user_group_attribute].user_group_id
         JOIN [guacamole_entity] ON [guacamole_user_group].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_group_permission] ON affected_user_group_id = [guacamole_user_group].user_group_id
         WHERE [guacamole_entity].name IN
             <foreach collection="identifiers" item="identifier"
                      open="(" separator="," close=")">
                 #{identifier,jdbcType=VARCHAR}
             </foreach>
             AND [guacamole_entity].type = 'USER_GROUP'
-            AND <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_group_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
-            AND permission = 'READ';
+            AND  [guacamole_user_group].user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            );
 
     </select>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
index 2092f24..b11a3c5 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserGroupMapper.xml
@@ -39,16 +39,15 @@
         FROM [guacamole_user_group_member]
         JOIN [guacamole_entity] ON [guacamole_entity].entity_id = [guacamole_user_group_member].member_entity_id
         JOIN [guacamole_user_group] ON [guacamole_user_group].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_group_permission] ON affected_user_group_id = [guacamole_user_group].user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_group_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            [guacamole_user_group].user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND [guacamole_user_group_member].user_group_id = #{parent.objectID,jdbcType=INTEGER}
             AND [guacamole_entity].type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete member groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
index 2c91c92..b0682b0 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupMemberUserMapper.xml
@@ -39,16 +39,15 @@
         FROM [guacamole_user_group_member]
         JOIN [guacamole_entity] ON [guacamole_entity].entity_id = [guacamole_user_group_member].member_entity_id
         JOIN [guacamole_user] ON [guacamole_user].entity_id = [guacamole_entity].entity_id
-        JOIN [guacamole_user_permission] ON affected_user_id = [guacamole_user].user_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            [guacamole_user].user_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.user.UserMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND [guacamole_user_group_member].user_group_id = #{parent.objectID,jdbcType=INTEGER}
             AND [guacamole_entity].type = 'USER'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete member users by name -->
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
index 0ea9252..198a624 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/usergroup/UserGroupParentUserGroupMapper.xml
@@ -40,16 +40,15 @@
         FROM [guacamole_user_group_member]
         JOIN [guacamole_user_group] ON [guacamole_user_group_member].user_group_id = [guacamole_user_group].user_group_id
         JOIN [guacamole_entity] ON [guacamole_entity].entity_id = [guacamole_user_group].entity_id
-        JOIN [guacamole_user_group_permission] ON affected_user_group_id = [guacamole_user_group].user_group_id
         WHERE
-            <include refid="org.apache.guacamole.auth.jdbc.base.EntityMapper.isRelatedEntity">
-                <property name="column"   value="[guacamole_user_group_permission].entity_id"/>
-                <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
-                <property name="groups"   value="effectiveGroups"/>
-            </include>
+            [guacamole_user_group].user_group_id IN (
+                <include refid="org.apache.guacamole.auth.jdbc.usergroup.UserGroupMapper.getReadableIDs">
+                    <property name="entityID" value="#{user.entityID,jdbcType=INTEGER}"/>
+                    <property name="groups"   value="effectiveGroups"/>
+                </include>
+            )
             AND [guacamole_user_group_member].member_entity_id = #{parent.entityID,jdbcType=INTEGER}
             AND [guacamole_entity].type = 'USER_GROUP'
-            AND permission = 'READ'
     </select>
 
     <!-- Delete parent groups by name -->
diff --git a/extensions/guacamole-auth-jdbc/pom.xml b/extensions/guacamole-auth-jdbc/pom.xml
index 2b6d536..5ac6b38 100644
--- a/extensions/guacamole-auth-jdbc/pom.xml
+++ b/extensions/guacamole-auth-jdbc/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-jdbc</artifactId>
     <packaging>pom</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-jdbc</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -81,7 +81,7 @@
             <dependency>
                 <groupId>org.apache.guacamole</groupId>
                 <artifactId>guacamole-ext</artifactId>
-                <version>1.2.0</version>
+                <version>1.3.0</version>
                 <scope>provided</scope>
             </dependency>
 
diff --git a/extensions/guacamole-auth-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml
index bf30ba8..e14e271 100644
--- a/extensions/guacamole-auth-ldap/pom.xml
+++ b/extensions/guacamole-auth-ldap/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-ldap</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-ldap</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -137,7 +137,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
index 7aeaeab..c473432 100644
--- a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "LDAP Authentication",
     "namespace" : "guac-ldap",
diff --git a/extensions/guacamole-auth-openid/pom.xml b/extensions/guacamole-auth-openid/pom.xml
index 8cc5ab9..691443a 100644
--- a/extensions/guacamole-auth-openid/pom.xml
+++ b/extensions/guacamole-auth-openid/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-openid</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-openid</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -177,7 +177,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java
index 8334326..30cd1ae 100644
--- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/AuthenticationProviderService.java
@@ -22,6 +22,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.Arrays;
+import java.util.Set;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.auth.openid.conf.ConfigurationService;
 import org.apache.guacamole.auth.openid.form.TokenField;
@@ -34,6 +35,7 @@
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
 import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
+import org.jose4j.jwt.JwtClaims;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -91,13 +93,19 @@
             throws GuacamoleException {
 
         String username = null;
+        Set<String> groups = null;
 
         // Validate OpenID token in request, if present, and derive username
         HttpServletRequest request = credentials.getRequest();
         if (request != null) {
             String token = request.getParameter(TokenField.PARAMETER_NAME);
-            if (token != null)
-                username = tokenService.processUsername(token);
+            if (token != null) {
+                JwtClaims claims = tokenService.validateToken(token);
+                if (claims != null) {
+                    username = tokenService.processUsername(claims);
+                    groups = tokenService.processGroups(claims);
+                }
+            }
         }
 
         // If the username was successfully retrieved from the token, produce
@@ -106,7 +114,7 @@
 
             // Create corresponding authenticated user
             AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
-            authenticatedUser.init(username, credentials);
+            authenticatedUser.init(username, credentials, groups);
             return authenticatedUser;
 
         }
diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java
index 9d889a8..68c22ef 100644
--- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java
+++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java
@@ -40,6 +40,12 @@
     private static final String DEFAULT_USERNAME_CLAIM_TYPE = "email";
 
     /**
+     * The default claim type to use to retrieve an authenticated user's
+     * groups.
+     */
+    private static final String DEFAULT_GROUPS_CLAIM_TYPE = "groups";
+
+    /**
      * The default space-separated list of OpenID scopes to request.
      */
     private static final String DEFAULT_SCOPE = "openid email profile";
@@ -109,6 +115,18 @@
     };
 
     /**
+     * The claim type which contains the authenticated user's groups within
+     * any valid JWT.
+     */
+    private static final StringGuacamoleProperty OPENID_GROUPS_CLAIM_TYPE =
+            new StringGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "openid-groups-claim-type"; }
+
+    };
+
+    /**
      * The space-separated list of OpenID scopes to request.
      */
     private static final StringGuacamoleProperty OPENID_SCOPE =
@@ -293,6 +311,22 @@
     }
 
     /**
+     * Returns the claim type which contains the authenticated user's groups
+     * within any valid JWT, as configured with guacamole.properties. By
+     * default, this will be "groups".
+     *
+     * @return
+     *     The claim type which contains the authenticated user's groups
+     *     within any valid JWT, as configured with guacamole.properties.
+     *
+     * @throws GuacamoleException
+     *     If guacamole.properties cannot be parsed.
+     */
+    public String getGroupsClaimType() throws GuacamoleException {
+        return environment.getProperty(OPENID_GROUPS_CLAIM_TYPE, DEFAULT_GROUPS_CLAIM_TYPE);
+    }
+
+    /**
      * Returns the space-separated list of OpenID scopes to request. By default,
      * this will be "openid email profile". The OpenID scopes determine the
      * information returned within the OpenID token, and thus affect what
diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java
index 5efb09d..72200df 100644
--- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java
+++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java
@@ -20,6 +20,10 @@
 package org.apache.guacamole.auth.openid.token;
 
 import com.google.inject.Inject;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 import org.apache.guacamole.auth.openid.conf.ConfigurationService;
 import org.apache.guacamole.GuacamoleException;
 import org.jose4j.jwk.HttpsJwks;
@@ -56,23 +60,20 @@
     private NonceService nonceService;
 
     /**
-     * Validates and parses the given ID token, returning the username contained
-     * therein, as defined by the username claim type given in
-     * guacamole.properties. If the username claim type is missing or the ID
-     * token is invalid, null is returned.
+     * Validates the given ID token, returning the JwtClaims contained therein.
+     * If the ID token is invalid, null is returned.
      *
      * @param token
-     *     The ID token to validate and parse.
+     *     The ID token to validate.
      *
      * @return
-     *     The username contained within the given ID token, or null if the ID
-     *     token is not valid or the username claim type is missing,
+     *     The JWT claims contained within the given ID token if it passes tests,
+     *     or null if the token is not valid.
      *
      * @throws GuacamoleException
      *     If guacamole.properties could not be parsed.
      */
-    public String processUsername(String token) throws GuacamoleException {
-
+    public JwtClaims validateToken(String token) throws GuacamoleException {
         // Validating the token requires a JWKS key resolver
         HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint().toString());
         HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks);
@@ -89,52 +90,115 @@
                 .build();
 
         try {
-
-            String usernameClaim = confService.getUsernameClaimType();
-
             // Validate JWT
             JwtClaims claims = jwtConsumer.processToClaims(token);
 
             // Verify a nonce is present
             String nonce = claims.getStringClaimValue("nonce");
-            if (nonce == null) {
+            if (nonce != null) {
+                // Verify that we actually generated the nonce, and that it has not
+                // already been used
+                if (nonceService.isValid(nonce)) {
+                    // nonce is valid, consider claims valid
+                    return claims;
+                }
+                else {
+                    logger.info("Rejected OpenID token with invalid/old nonce.");
+                }
+            }
+            else {
                 logger.info("Rejected OpenID token without nonce.");
-                return null;
             }
+        }
+        // Log any failures to validate/parse the JWT
+        catch (MalformedClaimException e) {
+            logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
+            logger.debug("Malformed claim within received JWT.", e);
+        }
+        catch (InvalidJwtException e) {
+            logger.info("Rejected invalid OpenID token: {}", e.getMessage());
+            logger.debug("Invalid JWT received.", e);
+        }
 
-            // Verify that we actually generated the nonce, and that it has not
-            // already been used
-            if (!nonceService.isValid(nonce)) {
-                logger.debug("Rejected OpenID token with invalid/old nonce.");
-                return null;
+        return null;
+    }
+
+    /**
+     * Parses the given JwtClaims, returning the username contained
+     * therein, as defined by the username claim type given in
+     * guacamole.properties. If the username claim type is missing or 
+     * is invalid, null is returned.
+     *
+     * @param claims
+     *     A valid JwtClaims to extract the username from.
+     *
+     * @return
+     *     The username contained within the given JwtClaims, or null if the
+     *     claim is not valid or the username claim type is missing,
+     *
+     * @throws GuacamoleException
+     *     If guacamole.properties could not be parsed.
+     */
+    public String processUsername(JwtClaims claims) throws GuacamoleException {
+        String usernameClaim = confService.getUsernameClaimType();
+
+        if (claims != null) {
+            try {
+                // Pull username from claims
+                String username = claims.getStringClaimValue(usernameClaim);
+                if (username != null)
+                    return username;
             }
-
-            // Pull username from claims
-            String username = claims.getStringClaimValue(usernameClaim);
-            if (username != null)
-                return username;
+            catch (MalformedClaimException e) {
+                logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
+                logger.debug("Malformed claim within received JWT.", e);
+            }
 
             // Warn if username was not present in token, as it likely means
             // the system is not set up correctly
             logger.warn("Username claim \"{}\" missing from token. Perhaps the "
                     + "OpenID scope and/or username claim type are "
                     + "misconfigured?", usernameClaim);
-
-        }
-
-        // Log any failures to validate/parse the JWT
-        catch (InvalidJwtException e) {
-            logger.info("Rejected invalid OpenID token: {}", e.getMessage());
-            logger.debug("Invalid JWT received.", e);
-        }
-        catch (MalformedClaimException e) {
-            logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
-            logger.debug("Malformed claim within received JWT.", e);
         }
 
         // Could not retrieve username from JWT
         return null;
-
     }
 
+    /**
+     * Parses the given JwtClaims, returning the groups contained
+     * therein, as defined by the groups claim type given in
+     * guacamole.properties. If the groups claim type is missing or
+     * is invalid, an empty set is returned.
+     *
+     * @param claims
+     *     A valid JwtClaims to extract groups from.
+     *
+     * @return
+     *     A Set of String representing the groups the user is member of
+     *     from the OpenID provider point of view, or an empty Set if
+     *     claim is not valid or the groups claim type is missing,
+     *
+     * @throws GuacamoleException
+     *     If guacamole.properties could not be parsed.
+     */
+    public Set<String> processGroups(JwtClaims claims) throws GuacamoleException {
+        String groupsClaim = confService.getGroupsClaimType();
+
+        if (claims != null) {
+            try {
+                // Pull groups from claims
+                List<String> oidcGroups = claims.getStringListClaimValue(groupsClaim);
+                if (oidcGroups != null && !oidcGroups.isEmpty())
+                    return Collections.unmodifiableSet(new HashSet<>(oidcGroups));
+            }   
+            catch (MalformedClaimException e) {
+                logger.info("Rejected OpenID token with malformed claim: {}", e.getMessage());
+                logger.debug("Malformed claim within received JWT.", e);
+            }
+        }
+
+        // Could not retrieve groups from JWT
+        return Collections.emptySet();
+    }
 }
diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java
index b7ff125..cfc9983 100644
--- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java
+++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/user/AuthenticatedUser.java
@@ -20,14 +20,15 @@
 package org.apache.guacamole.auth.openid.user;
 
 import com.google.inject.Inject;
+import java.util.Set;
 import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
 
 /**
  * An openid-specific implementation of AuthenticatedUser, associating a
- * username and particular set of credentials with the OpenID authentication
- * provider.
+ * username, a particular set of credentials and the groups with the 
+ * OpenID authentication provider.
  */
 public class AuthenticatedUser extends AbstractAuthenticatedUser {
 
@@ -44,6 +45,11 @@
     private Credentials credentials;
 
     /**
+     * The groups of the user that was authenticated.
+     */
+    private Set<String> effectiveGroups;
+
+    /**
      * Initializes this AuthenticatedUser using the given username and
      * credentials.
      *
@@ -52,9 +58,13 @@
      *
      * @param credentials
      *     The credentials provided when this user was authenticated.
+     *
+     * @param effectiveGroups
+     *     The groups of the user that was authenticated.
      */
-    public void init(String username, Credentials credentials) {
+    public void init(String username, Credentials credentials, Set<String> effectiveGroups) {
         this.credentials = credentials;
+        this.effectiveGroups = effectiveGroups;
         setIdentifier(username);
     }
 
@@ -68,4 +78,8 @@
         return credentials;
     }
 
+    @Override
+    public Set<String> getEffectiveUserGroups() {
+        return effectiveGroups;
+    }
 }
diff --git a/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
index ab35dc6..60c6f77 100644
--- a/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "OpenID Authentication Extension",
     "namespace" : "guac-openid",
@@ -10,6 +10,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/ja.json",
diff --git a/extensions/guacamole-auth-openid/src/main/resources/translations/ca.json b/extensions/guacamole-auth-openid/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..b1b8b5b
--- /dev/null
+++ b/extensions/guacamole-auth-openid/src/main/resources/translations/ca.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_OPENID" : {
+        "NAME" : "OpenID SSO Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_ID_TOKEN" : "",
+        "INFO_OID_REDIRECT_PENDING" : "Espereu, redirigint al proveïdor d'identitat ..."
+    }
+
+}
diff --git a/extensions/guacamole-auth-quickconnect/pom.xml b/extensions/guacamole-auth-quickconnect/pom.xml
index e989430..166f98f 100644
--- a/extensions/guacamole-auth-quickconnect/pom.xml
+++ b/extensions/guacamole-auth-quickconnect/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-quickconnect</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-quickconnect</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -188,7 +188,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
index 91ef44e..62200b4 100644
--- a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
@@ -1,5 +1,5 @@
 {
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"             : "Adhoc Guacamole Connections",
     "namespace"        : "quickconnect",
@@ -21,6 +21,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/ja.json",
diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/translations/ca.json b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..8a55ac9
--- /dev/null
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/ca.json
@@ -0,0 +1,18 @@
+{
+
+    "DATA_SOURCE_QUICKCONNECT" : {
+        "NAME" : "QuickConnect"
+    },
+
+    "QUICKCONNECT" : {
+        "ACTION_CONNECT"        : "Connecta’t",
+        
+        "ERROR_INVALID_URI"      : "Especificació URI no vàlida",
+        "ERROR_NO_HOST"          : "No s'ha especificat l'amfitrió",
+        "ERROR_NO_PROTOCOL"      : "No s'ha especificat protocol",
+        "ERROR_NOT_ABSOLUTE_URI" : "L'URI no és absolut",
+        
+        "FIELD_PLACEHOLDER_URI" : "Introduïu l'URI de connexió"
+    }
+
+}
diff --git a/extensions/guacamole-auth-radius/pom.xml b/extensions/guacamole-auth-radius/pom.xml
index 296ab47..675ed08 100644
--- a/extensions/guacamole-auth-radius/pom.xml
+++ b/extensions/guacamole-auth-radius/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-radius</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-radius</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -178,7 +178,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
         
diff --git a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
index d62f16c..0ea8538 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "RADIUS Authentication Backend",
     "namespace" : "radius",
@@ -10,6 +10,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/ja.json",
diff --git a/extensions/guacamole-auth-radius/src/main/resources/translations/ca.json b/extensions/guacamole-auth-radius/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..1ba2623
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/resources/translations/ca.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_RADIUS" : {
+        "NAME" : "RADIUS Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_RADIUS_STATE"              : "",
+        "FIELD_HEADER_RADIUSCHALLENGE"                : ""
+    }
+
+}
diff --git a/extensions/guacamole-auth-saml/pom.xml b/extensions/guacamole-auth-saml/pom.xml
index 135ffac..77671c7 100644
--- a/extensions/guacamole-auth-saml/pom.xml
+++ b/extensions/guacamole-auth-saml/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-saml</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-saml</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -132,7 +132,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-saml/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-saml/src/main/resources/guac-manifest.json
index bdb6d94..eea9d7b 100644
--- a/extensions/guacamole-auth-saml/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-saml/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "SAML Authentication Extension",
     "namespace" : "saml",
@@ -10,6 +10,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/en.json"
     ]
 
diff --git a/extensions/guacamole-auth-saml/src/main/resources/translations/ca.json b/extensions/guacamole-auth-saml/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..eef43e6
--- /dev/null
+++ b/extensions/guacamole-auth-saml/src/main/resources/translations/ca.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_SAML" : {
+        "NAME" : "Extensión de autenticación SAML"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_SAML"     : "",
+        "INFO_SAML_REDIRECT_PENDING" : "Por favor espere, redirigiendo al proveedor de identidad ..."
+    }
+
+}
diff --git a/extensions/guacamole-auth-totp/pom.xml b/extensions/guacamole-auth-totp/pom.xml
index d83e2ca..261ded2 100644
--- a/extensions/guacamole-auth-totp/pom.xml
+++ b/extensions/guacamole-auth-totp/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-totp</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-auth-totp</name>
     <url>http://guacamole.incubator.apache.org/</url>
 
@@ -217,7 +217,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <scope>provided</scope>
         </dependency>
         
diff --git a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
index ec1d5ac..6256d57 100644
--- a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.2.0",
+    "guacamoleVersion" : "1.3.0",
 
     "name"      : "TOTP TFA Authentication Backend",
     "namespace" : "totp",
@@ -10,6 +10,7 @@
     ],
 
     "translations" : [
+        "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
         "translations/ja.json",
diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/ca.json b/extensions/guacamole-auth-totp/src/main/resources/translations/ca.json
new file mode 100644
index 0000000..c9f4932
--- /dev/null
+++ b/extensions/guacamole-auth-totp/src/main/resources/translations/ca.json
@@ -0,0 +1,34 @@
+{
+
+    "DATA_SOURCE_TOTP" : {
+        "NAME" : "TOTP TFA Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_TOTP" : ""
+    },
+
+    "TOTP" : {
+
+        "ACTION_HIDE_DETAILS" : "Amaga",
+        "ACTION_SHOW_DETAILS" : "Mostra",
+
+        "FIELD_HEADER_ALGORITHM"  : "Algoritme:",
+        "FIELD_HEADER_DIGITS"     : "Digits:",
+        "FIELD_HEADER_INTERVAL"   : "Interval:",
+        "FIELD_HEADER_SECRET_KEY" : "Clau secreta:",
+
+        "FIELD_PLACEHOLDER_CODE" : "Codi d'autenticació",
+
+        "INFO_CODE_REQUIRED"       : "Introduïu el vostre codi d'autenticació per verificar la vostra identitat.",
+        "INFO_ENROLL_REQUIRED"     : "S'ha activat l'autenticació multi-factor al vostre compte.",
+        "INFO_VERIFICATION_FAILED" : "Ha fallat la verificació. Torna-ho a provar.",
+
+        "HELP_ENROLL_BARCODE" : "Per completar el procés d'inscripció, busqueu el codi de barres següent amb l'aplicació d'autenticació de dos factors al vostre telèfon o dispositiu.",
+        "HELP_ENROLL_VERIFY"  : "Després d'escanejar el codi de barres, introduïu el codi d'autentificació de {DIGITS} que apareix per comprovar que la inscripció ha tingut èxit.",
+
+        "SECTION_HEADER_DETAILS" : "Detalls:"
+
+    }
+
+}
diff --git a/guacamole-common-js/pom.xml b/guacamole-common-js/pom.xml
index 619868d..e38ea1c 100644
--- a/guacamole-common-js/pom.xml
+++ b/guacamole-common-js/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-common-js</artifactId>
     <packaging>pom</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-common-js</name>
     <url>http://guacamole.apache.org/</url>
 
diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js
index ed8085d..18d8412 100644
--- a/guacamole-common-js/src/main/webapp/modules/Client.js
+++ b/guacamole-common-js/src/main/webapp/modules/Client.js
@@ -713,6 +713,18 @@
      * @param {String} name The name of the pipe.
      */
     this.onpipe = null;
+    
+    /**
+     * Fired when a "required" instruction is received. A required instruction
+     * indicates that additional parameters are required for the connection to
+     * continue, such as user credentials.
+     * 
+     * @event
+     * @param {String[]} parameters
+     *      The names of the connection parameters that are required to be
+     *      provided for the connection to continue.
+     */
+    this.onrequired = null;
 
     /**
      * Fired whenever a sync instruction is received from the server, indicating
@@ -1337,6 +1349,10 @@
             display.rect(layer, x, y, w, h);
 
         },
+                
+        "required": function required(parameters) {
+            if (guac_client.onrequired) guac_client.onrequired(parameters);
+        },
         
         "reset": function(parameters) {
 
diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
index 149dce4..1464266 100644
--- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js
+++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
@@ -74,6 +74,20 @@
     };
 
     /**
+     * Changes the stored UUID that uniquely identifies this tunnel, firing the
+     * onuuid event if a handler has been defined.
+     *
+     * @private
+     * @param {String} uuid
+     *     The new state of this tunnel.
+     */
+    this.setUUID = function setUUID(uuid) {
+        this.uuid = uuid;
+        if (this.onuuid)
+            this.onuuid(uuid);
+    };
+
+    /**
      * Returns whether this tunnel is currently connected.
      *
      * @returns {Boolean}
@@ -120,6 +134,15 @@
     this.uuid = null;
 
     /**
+     * Fired when the UUID that uniquely identifies this tunnel is known.
+     *
+     * @event
+     * @param {String}
+     *     The UUID uniquely identifying this tunnel.
+     */
+    this.onuuid = null;
+
+    /**
      * Fired whenever an error is encountered by the tunnel.
      * 
      * @event
@@ -706,7 +729,7 @@
             reset_timeout();
 
             // Get UUID from response
-            tunnel.uuid = connect_xmlhttprequest.responseText;
+            tunnel.setUUID(connect_xmlhttprequest.responseText);
 
             // Mark as open
             tunnel.setState(Guacamole.Tunnel.State.OPEN);
@@ -1019,7 +1042,7 @@
 
                         // Associate tunnel UUID if received
                         if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE)
-                            tunnel.uuid = elements[0];
+                            tunnel.setUUID(elements[0]);
 
                         // Tunnel is now open and UUID is available
                         tunnel.setState(Guacamole.Tunnel.State.OPEN);
@@ -1155,11 +1178,18 @@
          * @private
          */
         function commit_tunnel() {
+
             tunnel.onstatechange = chained_tunnel.onstatechange;
             tunnel.oninstruction = chained_tunnel.oninstruction;
             tunnel.onerror = chained_tunnel.onerror;
-            chained_tunnel.uuid = tunnel.uuid;
+            tunnel.onuuid = chained_tunnel.onuuid;
+
+            // Assign UUID if already known
+            if (tunnel.uuid)
+                chained_tunnel.setUUID(tunnel.uuid);
+
             committedTunnel = tunnel;
+
         }
 
         // Wrap own onstatechange within current tunnel
diff --git a/guacamole-common-js/src/main/webapp/modules/Version.js b/guacamole-common-js/src/main/webapp/modules/Version.js
index f8a5b3b..4bad5a2 100644
--- a/guacamole-common-js/src/main/webapp/modules/Version.js
+++ b/guacamole-common-js/src/main/webapp/modules/Version.js
@@ -27,4 +27,4 @@
  *
  * @type {String}
  */
-Guacamole.API_VERSION = "1.2.0";
+Guacamole.API_VERSION = "1.3.0";
diff --git a/guacamole-common/pom.xml b/guacamole-common/pom.xml
index 2b4f353..f9fb82a 100644
--- a/guacamole-common/pom.xml
+++ b/guacamole-common/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-common</artifactId>
     <packaging>jar</packaging>
-    <version>1.1.0</version>
+    <version>1.3.0</version>
     <name>guacamole-common</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -57,14 +57,14 @@
     <build>
         <plugins>
 
-            <!-- Written for 1.6 -->
+            <!-- Written for 1.8 -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <version>3.3</version>
                 <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
+                    <source>1.8</source>
+                    <target>1.8</target>
                     <compilerArgs>
                         <arg>-Xlint:all</arg>
                         <arg>-Werror</arg>
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/net/DelegatingGuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/net/DelegatingGuacamoleSocket.java
new file mode 100644
index 0000000..b519629
--- /dev/null
+++ b/guacamole-common/src/main/java/org/apache/guacamole/net/DelegatingGuacamoleSocket.java
@@ -0,0 +1,84 @@
+/*
+ * 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.net;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.io.GuacamoleReader;
+import org.apache.guacamole.io.GuacamoleWriter;
+
+/**
+ * GuacamoleSocket implementation which simply delegates all function calls to
+ * an underlying GuacamoleSocket.
+ */
+public class DelegatingGuacamoleSocket implements GuacamoleSocket {
+
+    /**
+     * The wrapped socket.
+     */
+    private final GuacamoleSocket socket;
+
+    /**
+     * Wraps the given GuacamoleSocket such that all function calls against
+     * this DelegatingGuacamoleSocket will be delegated to it.
+     *
+     * @param socket
+     *     The GuacamoleSocket to wrap.
+     */
+    public DelegatingGuacamoleSocket(GuacamoleSocket socket) {
+        this.socket = socket;
+    }
+
+    /**
+     * Returns the underlying GuacamoleSocket wrapped by this
+     * DelegatingGuacamoleSocket.
+     *
+     * @return
+     *     The GuacamoleSocket wrapped by this DelegatingGuacamoleSocket.
+     */
+    protected GuacamoleSocket getDelegateSocket() {
+        return socket;
+    }
+
+    @Override
+    public String getProtocol() {
+        return socket.getProtocol();
+    }
+
+    @Override
+    public GuacamoleReader getReader() {
+        return socket.getReader();
+    }
+
+    @Override
+    public GuacamoleWriter getWriter() {
+        return socket.getWriter();
+    }
+
+    @Override
+    public void close() throws GuacamoleException {
+        socket.close();
+    }
+
+    @Override
+    public boolean isOpen() {
+        return socket.isOpen();
+    }
+
+}
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleSocket.java
index 4d084e4..af068fa 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleSocket.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/net/GuacamoleSocket.java
@@ -30,6 +30,24 @@
 public interface GuacamoleSocket {
 
     /**
+     * Returns the name of the protocol to be used. If the protocol is not
+     * known or the implementation refuses to reveal the underlying protocol,
+     * null is returned.
+     *
+     * <p>Implementations <strong>should</strong> aim to expose the name of the
+     * underlying protocol, such that protocol-specific responses like the
+     * "required" and "argv" instructions can be handled correctly by code
+     * consuming the GuacamoleSocket.
+     *
+     * @return
+     *     The name of the protocol to be used, or null if this information is
+     *     not available.
+     */
+    public default String getProtocol() {
+        return null;
+    }
+
+    /**
      * Returns a GuacamoleReader which can be used to read from the
      * Guacamole instruction stream associated with the connection
      * represented by this GuacamoleSocket.
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
index fe4efca..6cf3d7b 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
@@ -24,6 +24,7 @@
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.io.GuacamoleReader;
 import org.apache.guacamole.io.GuacamoleWriter;
+import org.apache.guacamole.net.DelegatingGuacamoleSocket;
 import org.apache.guacamole.net.GuacamoleSocket;
 
 /**
@@ -36,12 +37,7 @@
  * this GuacamoleSocket from manually controlling the initial protocol
  * handshake.
  */
-public class ConfiguredGuacamoleSocket implements GuacamoleSocket {
-
-    /**
-     * The wrapped socket.
-     */
-    private GuacamoleSocket socket;
+public class ConfiguredGuacamoleSocket extends DelegatingGuacamoleSocket {
 
     /**
      * The configuration to use when performing the Guacamole protocol
@@ -125,7 +121,7 @@
             GuacamoleConfiguration config,
             GuacamoleClientInformation info) throws GuacamoleException {
 
-        this.socket = socket;
+        super(socket);
         this.config = config;
 
         // Get reader and writer
@@ -268,23 +264,8 @@
     }
 
     @Override
-    public GuacamoleWriter getWriter() {
-        return socket.getWriter();
-    }
-
-    @Override
-    public GuacamoleReader getReader() {
-        return socket.getReader();
-    }
-
-    @Override
-    public void close() throws GuacamoleException {
-        socket.close();
-    }
-
-    @Override
-    public boolean isOpen() {
-        return socket.isOpen();
+    public String getProtocol() {
+        return getConfiguration().getProtocol();
     }
 
 }
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
index 3c64c51..15414c0 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
@@ -28,7 +28,7 @@
 import org.apache.guacamole.GuacamoleUpstreamTimeoutException;
 import org.apache.guacamole.GuacamoleUpstreamUnavailableException;
 import org.apache.guacamole.io.GuacamoleReader;
-import org.apache.guacamole.io.GuacamoleWriter;
+import org.apache.guacamole.net.DelegatingGuacamoleSocket;
 import org.apache.guacamole.net.GuacamoleSocket;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -40,7 +40,7 @@
  * constructor, allowing a different socket to be substituted prior to
  * fulfilling the connection.
  */
-public class FailoverGuacamoleSocket implements GuacamoleSocket {
+public class FailoverGuacamoleSocket extends DelegatingGuacamoleSocket {
 
     /**
      * Logger for this class.
@@ -55,11 +55,6 @@
     private static final int DEFAULT_INSTRUCTION_QUEUE_LIMIT = 131072;
 
     /**
-     * The wrapped socket being used.
-     */
-    private final GuacamoleSocket socket;
-
-    /**
      * Queue of all instructions read while this FailoverGuacamoleSocket was
      * being constructed.
      */
@@ -158,6 +153,8 @@
             final int instructionQueueLimit)
             throws GuacamoleException, GuacamoleUpstreamException {
 
+        super(socket);
+
         int totalQueueSize = 0;
 
         GuacamoleInstruction instruction;
@@ -189,8 +186,6 @@
 
         }
 
-        this.socket = socket;
-
     }
 
     /**
@@ -230,7 +225,7 @@
 
         @Override
         public boolean available() throws GuacamoleException {
-            return !instructionQueue.isEmpty() || socket.getReader().available();
+            return !instructionQueue.isEmpty() || getDelegateSocket().getReader().available();
         }
 
         @Override
@@ -244,7 +239,7 @@
                 return instruction.toString().toCharArray();
             }
 
-            return socket.getReader().read();
+            return getDelegateSocket().getReader().read();
 
         }
 
@@ -258,7 +253,7 @@
             if (!instructionQueue.isEmpty())
                 return instructionQueue.remove();
 
-            return socket.getReader().readInstruction();
+            return getDelegateSocket().getReader().readInstruction();
 
         }
 
@@ -269,19 +264,4 @@
         return queuedReader;
     }
 
-    @Override
-    public GuacamoleWriter getWriter() {
-        return socket.getWriter();
-    }
-
-    @Override
-    public void close() throws GuacamoleException {
-        socket.close();
-    }
-
-    @Override
-    public boolean isOpen() {
-        return socket.isOpen();
-    }
-    
 }
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/FilteredGuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FilteredGuacamoleSocket.java
index 5e541d0..c3bfefd 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/FilteredGuacamoleSocket.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FilteredGuacamoleSocket.java
@@ -19,21 +19,16 @@
 
 package org.apache.guacamole.protocol;
 
-import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.io.GuacamoleReader;
 import org.apache.guacamole.io.GuacamoleWriter;
+import org.apache.guacamole.net.DelegatingGuacamoleSocket;
 import org.apache.guacamole.net.GuacamoleSocket;
 
 /**
  * Implementation of GuacamoleSocket which allows individual instructions to be
  * intercepted, overridden, etc.
  */
-public class FilteredGuacamoleSocket implements GuacamoleSocket {
-
-    /**
-     * Wrapped GuacamoleSocket.
-     */
-    private final GuacamoleSocket socket;
+public class FilteredGuacamoleSocket extends DelegatingGuacamoleSocket {
 
     /**
      * A reader for the wrapped GuacamoleSocket which may be filtered.
@@ -58,7 +53,8 @@
      *                    instructions, if any.
      */
     public FilteredGuacamoleSocket(GuacamoleSocket socket, GuacamoleFilter readFilter, GuacamoleFilter writeFilter) {
-        this.socket = socket;
+
+        super(socket);
 
         // Apply filter to reader
         if (readFilter != null)
@@ -84,14 +80,4 @@
         return writer;
     }
 
-    @Override
-    public void close() throws GuacamoleException {
-        socket.close();
-    }
-
-    @Override
-    public boolean isOpen() {
-        return socket.isOpen();
-    }
-    
 }
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleConfiguration.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleConfiguration.java
index 48d7e52..5c96066 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleConfiguration.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleConfiguration.java
@@ -92,8 +92,7 @@
 
     /**
      * Sets the ID of the connection being joined, if any. If no connection
-     * is being joined, this value must be omitted, and the protocol must be
-     * set instead.
+     * is being joined, this value must be omitted.
      *
      * @param connectionID The ID of the connection being joined.
      */
@@ -103,15 +102,34 @@
 
     /**
      * Returns the name of the protocol to be used.
-     * @return The name of the protocol to be used.
+     *
+     * @return
+     *     The name of the protocol to be used.
      */
     public String getProtocol() {
         return protocol;
     }
 
     /**
-     * Sets the name of the protocol to be used.
-     * @param protocol The name of the protocol to be used.
+     * Sets the name of the protocol to be used. If no connection is being
+     * joined (a new connection is being established), this value must be set.
+     *
+     * <p>If a connection is being joined, <strong>this value should still be
+     * set</strong> to ensure that protocol-specific responses like the
+     * "required" and "argv" instructions can be understood in their proper
+     * context by other code that may consume this GuacamoleConfiguration like
+     * {@link ConfiguredGuacamoleSocket}.
+     *
+     * <p>If this value is unavailable or remains unset, it is still possible
+     * to join an established connection using
+     * {@link #setConnectionID(java.lang.String)}, however protocol-specific
+     * responses like the "required" and "argv" instructions might not be
+     * possible to handle correctly if the underlying protocol is not made
+     * available through some other means to the client receiving those
+     * responses.
+     *
+     * @param protocol
+     *    The name of the protocol to be used.
      */
     public void setProtocol(String protocol) {
         this.protocol = protocol;
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java
index 79f73f8..5cad8a2 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java
@@ -40,7 +40,16 @@
      * {@link GuacamoleProtocolVersion#VERSION_1_1_0}.
      */
     PROTOCOL_VERSION_DETECTION(GuacamoleProtocolVersion.VERSION_1_1_0),
-    
+
+    /**
+     * Support for the "required" instruction. The "required" instruction
+     * allows the server to explicitly request connection parameters from the
+     * client without which the connection cannot continue, such as user
+     * credentials. Support for this instruction was introduced in
+     * {@link GuacamoleProtocolVersion#VERSION_1_3_0}.
+     */
+    REQUIRED_INSTRUCTION(GuacamoleProtocolVersion.VERSION_1_3_0),
+
     /**
      * Support for the "timezone" handshake instruction. The "timezone"
      * instruction allows the client to request that the server forward their
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java
index c1d50ba..3ceda3b 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java
@@ -47,10 +47,17 @@
     public static final GuacamoleProtocolVersion VERSION_1_1_0 = new GuacamoleProtocolVersion(1, 1, 0);
 
     /**
+     * Protocol version 1.3.0, which introduces the "required" instruction
+     * allowing the server to explicitly request connection parameters from the
+     * client.
+     */
+    public static final GuacamoleProtocolVersion VERSION_1_3_0 = new GuacamoleProtocolVersion(1, 3, 0);
+
+    /**
      * The most recent version of the Guacamole protocol at the time this
      * version of GuacamoleProtocolVersion was built.
      */
-    public static final GuacamoleProtocolVersion LATEST = VERSION_1_1_0;
+    public static final GuacamoleProtocolVersion LATEST = VERSION_1_3_0;
     
     /**
      * A regular expression that matches the VERSION_X_Y_Z pattern, where
diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh
index 5123a8a..e58ba9e 100755
--- a/guacamole-docker/bin/start.sh
+++ b/guacamole-docker/bin/start.sh
@@ -678,6 +678,10 @@
     set_property            "cas-authorization-endpoint"       "$CAS_AUTHORIZATION_ENDPOINT"
     set_property            "cas-redirect-uri"                 "$CAS_REDIRECT_URI"
     set_optional_property   "cas-clearpass-key"                "$CAS_CLEARPASS_KEY"
+    set_optional_property   "cas-group-attribute"              "$CAS_GROUP_ATTRIBUTE"
+    set_optional_property   "cas-group-format"                 "$CAS_GROUP_FORMAT"
+    set_optional_property   "cas-group-ldap-base-dn"           "$CAS_GROUP_LDAP_BASE_DN"
+    set_optional_property   "cas-group-ldap-attribute"         "$CAS_GROUP_LDAP_ATTRIBUTE"
 
     # Add required .jar files to GUACAMOLE_EXT
     ln -s /opt/guacamole/cas/guacamole-auth-*.jar   "$GUACAMOLE_EXT"
diff --git a/guacamole-ext/pom.xml b/guacamole-ext/pom.xml
index adb12e9..d5717f5 100644
--- a/guacamole-ext/pom.xml
+++ b/guacamole-ext/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-ext</artifactId>
     <packaging>jar</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-ext</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -155,7 +155,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.1.0</version>
+            <version>1.3.0</version>
             <scope>compile</scope>
         </dependency>
 
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUser.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUser.java
index b502f7a..f4e85ae 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUser.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AbstractUser.java
@@ -21,7 +21,6 @@
 
 import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
@@ -85,18 +84,7 @@
     public Date getLastActive() {
         return null;
     }
-
-    /**
-     * {@inheritDoc}
-     *
-     * <p>This implementation simply an immutable, empty list. Implementations
-     * that wish to expose user login history should override this function.
-     */
-    @Override
-    public List<ActivityRecord> getHistory() throws GuacamoleException {
-        return Collections.emptyList();
-    }
-
+    
     /**
      * {@inheritDoc}
      *
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java
index 5b1d13d..3d8e64d 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java
@@ -19,10 +19,13 @@
 
 package org.apache.guacamole.net.auth;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Set;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleUnsupportedException;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
 
 /**
@@ -98,18 +101,57 @@
      * of this Connection, including any active users. ConnectionRecords
      * in this list will be sorted in descending order of end time (active
      * connections are first), and then in descending order of start time
-     * (newer connections are first).
+     * (newer connections are first). If connection history tracking is
+     * not implemented this method should throw GuacamoleUnsupportedException.
      *
-     * @return A list of ConnectionRecrods representing the usage history
-     *         of this Connection.
+     * @deprecated 
+     *     This function has been deprecated in favor of
+     *     {@link getConnectionHistory}, which returns the connection history
+     *     as an ActivityRecordSet that can be easily sorted and filtered.
+     *     While the getHistory() method is provided for API compatibility,
+     *     new implementations should avoid use of this method and, instead,
+     *     implement the getConnectionHistory() method.
+     * 
+     * @return
+     *     A list of ConnectionRecrods representing the usage history of this
+     *     Connection.
      *
-     * @throws GuacamoleException If an error occurs while reading the history
-     *                            of this connection, or if permission is
-     *                            denied.
+     * @throws GuacamoleException
+     *     If history tracking is not implemented, if an error occurs while
+     *     reading the history of this connection, or if permission is
+     *     denied.
      */
-    public List<? extends ConnectionRecord> getHistory() throws GuacamoleException;
+    @Deprecated
+    default List<? extends ConnectionRecord> getHistory()
+            throws GuacamoleException {
+        return Collections.unmodifiableList(new ArrayList<>(getConnectionHistory().asCollection()));
+    }
 
     /**
+     * Returns an ActivityRecordSet containing ConnectionRecords that
+     * represent the usage history of this Connection, including any active
+     * users. ConnectionRecords in this list will be sorted in descending order
+     * of end time (active connections are first), and then in descending order
+     * of start time (newer connections are first). If connection history
+     * tracking has not been implemented, or has been implemented using the
+     * deprecated {@link getHistory} method, this function should throw
+     * GuacamoleUnsupportedExpcetion.
+     * 
+     * @return
+     *     An ActivityRecordSet containing ConnectionRecords representing the
+     *     usage history of this Connection.
+     * 
+     * @throws GuacamoleException
+     *     If history tracking is not implemented, if an error occurs while
+     *     reading the history of this connection, or if permission is
+     *     denied.
+     */
+    default ActivityRecordSet<ConnectionRecord> getConnectionHistory()
+            throws GuacamoleException {
+        throw new GuacamoleUnsupportedException("This implementation of Connection does not provide connection history.");
+    }
+    
+    /**
      * Returns identifiers of all readable sharing profiles that can be used to
      * join this connection when it is active. The level of access granted to a
      * joining user is dictated by the connection parameters associated with
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java
index 95b6e93..cc99baa 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java
@@ -134,11 +134,18 @@
         return connection.getLastActive();
     }
 
+    @Deprecated
     @Override
     public List<? extends ConnectionRecord> getHistory()
             throws GuacamoleException {
         return connection.getHistory();
     }
+    
+    @Override
+    public ActivityRecordSet<ConnectionRecord> getConnectionHistory()
+            throws GuacamoleException {
+        return connection.getConnectionHistory();
+    }
 
     @Override
     public Set<String> getSharingProfileIdentifiers()
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUser.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUser.java
index add7be6..9da0fb5 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUser.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUser.java
@@ -93,11 +93,18 @@
         return user.getLastActive();
     }
 
+    @Deprecated
     @Override
     public List<? extends ActivityRecord> getHistory()
             throws GuacamoleException {
         return user.getHistory();
     }
+    
+    @Override
+    public ActivityRecordSet<ActivityRecord> getUserHistory()
+            throws GuacamoleException {
+        return user.getUserHistory();
+    }
 
     @Override
     public SystemPermissionSet getSystemPermissions()
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/User.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/User.java
index 4a8b43a..788e8d6 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/User.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/User.java
@@ -19,9 +19,12 @@
 
 package org.apache.guacamole.net.auth;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleUnsupportedException;
 
 /**
  * A user of the Guacamole web application.
@@ -93,17 +96,50 @@
      * of this user, including any active sessions. ActivityRecords
      * in this list will be sorted in descending order of end time (active
      * sessions are first), and then in descending order of start time
-     * (newer sessions are first).
+     * (newer sessions are first). If user login history is not implemented
+     * this method should throw GuacamoleUnsupportedException.
      *
+     * @deprecated
+     *     This function is deprecated in favor of {@link getUserHistory}, which
+     *     returns the login history as an ActivityRecordSet which supports
+     *     various sort and filter functions. While this continues to be defined
+     *     for API compatibility, new implementation should avoid this function
+     *     and use getUserHistory(), instead.
+     * 
      * @return
      *     A list of ActivityRecords representing the login history of this
      *     User.
      *
      * @throws GuacamoleException
-     *     If an error occurs while reading the history of this user, or if
-     *     permission is denied.
+     *     If history tracking is not implemented, if an error occurs while
+     *     reading the history of this user, or if permission is denied.
      */
-    List<? extends ActivityRecord> getHistory() throws GuacamoleException;
+    @Deprecated
+    default List<? extends ActivityRecord> getHistory() throws GuacamoleException {
+        return Collections.unmodifiableList(new ArrayList<>(getUserHistory().asCollection()));
+    }
+    
+    /**
+     * Returns an ActivityRecordSet containing ActivityRecords representing
+     * the login history for this user, including any active sessions.
+     * ActivityRecords in this list will be sorted in descending order of end
+     * time (active sessions are first), and then in descending order of start
+     * time (newer sessions are first). If login history tracking is not
+     * implemented, or is only implemented using the deprecated {@link getHistory}
+     * method, this method should throw GuacamoleUnsupportedException.
+     * 
+     * @return
+     *     An ActivityRecordSet containing ActivityRecords representing the
+     *     login history for this user.
+     * 
+     * @throws GuacamoleException
+     *     If history tracking is not implemented, if an error occurs while
+     *     reading the history of this user, or if permission is denied.
+     */
+    default ActivityRecordSet<ActivityRecord> getUserHistory()
+            throws GuacamoleException {
+        throw new GuacamoleUnsupportedException("The default implementation of User does not provide login history.");
+    }
 
     /**
      * Returns a set of all readable user groups of which this user is a member.
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleActivityRecordSet.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleActivityRecordSet.java
index a9a3c3e..21aca57 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleActivityRecordSet.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleActivityRecordSet.java
@@ -35,10 +35,34 @@
 public class SimpleActivityRecordSet<RecordType extends ActivityRecord>
         implements ActivityRecordSet<RecordType> {
 
+    /**
+     * The records associated with this record set, if any.
+     */
+    private final Collection<RecordType> records;
+    
+    /**
+     * Create a new SimpleActivityRecordSet that contains an empty set of
+     * records.
+     */
+    public SimpleActivityRecordSet() {
+        records = Collections.emptyList();
+    }
+    
+    /**
+     * Create a new SimpleActivityRecordSet that contains the provided records
+     * which will back this record set.
+     * 
+     * @param records 
+     *     The records that this SimpleActivityRecordSet should contain.
+     */
+    public SimpleActivityRecordSet(Collection<? extends RecordType> records) {
+        this.records = Collections.unmodifiableCollection(records);
+    }
+    
     @Override
     public Collection<RecordType> asCollection()
             throws GuacamoleException {
-        return Collections.<RecordType>emptyList();
+        return records;
     }
 
     @Override
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java
index 2934cbe..ba61f7a 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java
@@ -21,7 +21,6 @@
 
 import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
@@ -33,6 +32,7 @@
 import org.apache.guacamole.net.SSLGuacamoleSocket;
 import org.apache.guacamole.net.SimpleGuacamoleTunnel;
 import org.apache.guacamole.net.auth.AbstractConnection;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
 import org.apache.guacamole.net.auth.ConnectionRecord;
 import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration;
 import org.apache.guacamole.protocol.ConfiguredGuacamoleSocket;
@@ -283,10 +283,11 @@
     public Date getLastActive() {
         return null;
     }
-
+    
     @Override
-    public List<ConnectionRecord> getHistory() throws GuacamoleException {
-        return Collections.<ConnectionRecord>emptyList();
+    public ActivityRecordSet<ConnectionRecord> getConnectionHistory()
+            throws GuacamoleException {
+        return new SimpleActivityRecordSet<>();
     }
 
 }
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java
index 943e7a1..d1570e4 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java
@@ -222,12 +222,12 @@
                     if (modifier != null && !modifier.isEmpty()) {
                         switch (modifier) {
                             // Switch token to upper-case
-                            case "upper":
+                            case "UPPER":
                                 output.append(tokenValue.toUpperCase());
                                 break;
                                 
                             // Switch token to lower case
-                            case "lower":
+                            case "LOWER":
                                 output.append(tokenValue.toLowerCase());
                                 break;
                                 
diff --git a/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenFilterTest.java b/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenFilterTest.java
index 7061e4f..116a860 100644
--- a/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenFilterTest.java
+++ b/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenFilterTest.java
@@ -60,12 +60,12 @@
         
         assertEquals(
             "value-of-c",
-            tokenFilter.filter("${TOKEN_C:lower}")
+            tokenFilter.filter("${TOKEN_C:LOWER}")
         );
         
         assertEquals(
             "VALUE-OF-C",
-            tokenFilter.filter("${TOKEN_C:upper}")
+            tokenFilter.filter("${TOKEN_C:UPPER}")
         );
         
     }
@@ -83,7 +83,7 @@
         tokenFilter.setToken("TOKEN_B", "value-of-b");
 
         // Create test map
-        Map<Integer, String> map = new HashMap<Integer, String>();
+        Map<Integer, String> map = new HashMap<>();
         map.put(1, "$$${NOPE}hello${TOKEN_A}world${TOKEN_B}$${NOT_A_TOKEN}");
         map.put(2, "${NOPE}hello${TOKEN_A}world${TOKEN_C}");
         map.put(3, null);
diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index 588b4fc..0693e6a 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole</artifactId>
     <packaging>war</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -264,14 +264,14 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
         </dependency>
 
         <!-- Guacamole JavaScript API -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.2.0</version>
+            <version>1.3.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
index 829e334..58f124e 100644
--- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
+++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
@@ -64,7 +64,8 @@
             "*",
             "1.0.0",
             "1.1.0",
-            "1.2.0"
+            "1.2.0",
+            "1.3.0"
         ));
 
     /**
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java b/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java
index 704db23..6b407b4 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java
@@ -19,15 +19,13 @@
 
 package org.apache.guacamole.rest.connection;
 
-import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleUnsupportedException;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.net.auth.Connection;
-import org.apache.guacamole.net.auth.ConnectionRecord;
 import org.apache.guacamole.protocol.GuacamoleClientInformation;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
 
@@ -123,24 +121,19 @@
     }
 
     @Override
-    public Set<String> getSharingProfileIdentifiers() {
-        throw new UnsupportedOperationException("Operation not supported.");
+    public Set<String> getSharingProfileIdentifiers() throws GuacamoleException {
+        throw new GuacamoleUnsupportedException("Operation not supported.");
     }
 
     @Override
     public GuacamoleTunnel connect(GuacamoleClientInformation info,
             Map<String, String> tokens) throws GuacamoleException {
-        throw new UnsupportedOperationException("Operation not supported.");
+        throw new GuacamoleUnsupportedException("Operation not supported.");
     }
 
     @Override
     public Date getLastActive() {
         return null;
     }
-
-    @Override
-    public List<? extends ConnectionRecord> getHistory() throws GuacamoleException {
-        return Collections.<ConnectionRecord>emptyList();
-    }
     
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java
index 3149987..7d4f0d5 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java
@@ -22,8 +22,6 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
@@ -32,8 +30,8 @@
 import javax.ws.rs.core.MediaType;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleSecurityException;
+import org.apache.guacamole.GuacamoleUnsupportedException;
 import org.apache.guacamole.net.auth.Connection;
-import org.apache.guacamole.net.auth.ConnectionRecord;
 import org.apache.guacamole.net.auth.Directory;
 import org.apache.guacamole.net.auth.Permissions;
 import org.apache.guacamole.rest.directory.DirectoryView;
@@ -43,13 +41,16 @@
 import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
 import org.apache.guacamole.net.auth.permission.SystemPermission;
 import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
-import org.apache.guacamole.rest.history.APIConnectionRecord;
+import org.apache.guacamole.net.auth.simple.SimpleActivityRecordSet;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
 import org.apache.guacamole.rest.directory.DirectoryObjectResource;
 import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
 import org.apache.guacamole.rest.directory.DirectoryResource;
 import org.apache.guacamole.rest.directory.DirectoryResourceFactory;
+import org.apache.guacamole.rest.history.ConnectionHistoryResource;
 import org.apache.guacamole.rest.sharingprofile.APISharingProfile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A REST resource which abstracts the operations available on an existing
@@ -60,6 +61,11 @@
 public class ConnectionResource extends DirectoryObjectResource<Connection, APIConnection> {
 
     /**
+     * Logger for this class.
+     */
+    private static final Logger logger = LoggerFactory.getLogger(ConnectionResource.class);
+    
+    /**
      * The UserContext associated with the Directory which contains the
      * Connection exposed by this resource.
      */
@@ -150,18 +156,29 @@
      * @throws GuacamoleException
      *     If an error occurs while retrieving the connection history.
      */
-    @GET
+    @SuppressWarnings("deprecation")
     @Path("history")
-    public List<APIConnectionRecord> getConnectionHistory()
+    public ConnectionHistoryResource getConnectionHistory()
             throws GuacamoleException {
 
-        // Retrieve the requested connection's history
-        List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>();
-        for (ConnectionRecord record : connection.getHistory())
-            apiRecords.add(new APIConnectionRecord(record));
-
-        // Return the converted history
-        return apiRecords;
+        // Try the current getConnectionHistory() method, first, for connection history.
+        try {
+            return new ConnectionHistoryResource(connection.getConnectionHistory());
+        }
+        catch (GuacamoleUnsupportedException e) {
+            logger.debug("Call to getConnectionHistory() is unsupported, falling back to getHistory().", e);
+        }
+        
+        // Fall back to the deprecated getHistory() method.
+        try {
+            return new ConnectionHistoryResource(new SimpleActivityRecordSet<>(connection.getHistory()));
+        }
+        catch (GuacamoleUnsupportedException e) {
+            logger.debug("Call to getHistory() is unsupported, no connection history records will be returned.", e);
+        }
+        
+        // If all fails, return an empty connection history set.
+        return new ConnectionHistoryResource(new SimpleActivityRecordSet<>());
 
     }
 
@@ -184,7 +201,7 @@
 
         // Produce subset of all SharingProfiles, containing only those which
         // are associated with this connection
-        Directory<SharingProfile> sharingProfiles = new DirectoryView<SharingProfile>(
+        Directory<SharingProfile> sharingProfiles = new DirectoryView<>(
             userContext.getSharingProfileDirectory(),
             connection.getSharingProfileIdentifiers()
         );
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java
index 8da8355..704988c 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java
@@ -24,7 +24,9 @@
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleUnsupportedException;
 import org.apache.guacamole.net.auth.UserContext;
+import org.apache.guacamole.net.auth.simple.SimpleActivityRecordSet;
 
 /**
  * A REST resource for retrieving and managing the history records of Guacamole
@@ -64,7 +66,12 @@
      */
     @Path("connections")
     public ConnectionHistoryResource getConnectionHistory() throws GuacamoleException {
-        return new ConnectionHistoryResource(userContext.getConnectionHistory());
+        try {
+            return new ConnectionHistoryResource(userContext.getConnectionHistory());
+        }
+        catch (GuacamoleUnsupportedException e) {
+            return new ConnectionHistoryResource(new SimpleActivityRecordSet<>());
+        }
     }
 
     /**
@@ -81,7 +88,12 @@
      */
     @Path("users")
     public UserHistoryResource getUserHistory() throws GuacamoleException {
-        return new UserHistoryResource(userContext.getUserHistory());
+        try {
+            return new UserHistoryResource(userContext.getUserHistory());
+        }
+        catch (GuacamoleUnsupportedException e) {
+            return new UserHistoryResource(new SimpleActivityRecordSet<>());
+        }
     }
 
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResource.java
index 5397339..74347b7 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResource.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResource.java
@@ -24,6 +24,7 @@
 import com.google.inject.assistedinject.AssistedInject;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -31,8 +32,10 @@
 import javax.ws.rs.core.MediaType;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleResourceNotFoundException;
+import org.apache.guacamole.environment.Environment;
 import org.apache.guacamole.net.auth.ActiveConnection;
 import org.apache.guacamole.net.auth.UserContext;
+import org.apache.guacamole.protocols.ProtocolInfo;
 import org.apache.guacamole.rest.activeconnection.APIActiveConnection;
 import org.apache.guacamole.rest.directory.DirectoryObjectResource;
 import org.apache.guacamole.rest.directory.DirectoryObjectResourceFactory;
@@ -58,6 +61,12 @@
     private final UserTunnel tunnel;
 
     /**
+     * The Guacamole server environment.
+     */
+    @Inject
+    private Environment environment;
+
+    /**
      * A factory which can be used to create instances of resources representing
      * ActiveConnections.
      */
@@ -107,6 +116,39 @@
     }
 
     /**
+     * Retrieves the underlying protocol used by the connection associated with
+     * this tunnel. If possible, the parameters available for that protocol are
+     * retrieved, as well.
+     *
+     * @return
+     *     A ProtocolInfo object describing the protocol used by the connection
+     *     associated with this tunnel.
+     *
+     * @throws GuacamoleException
+     *     If the protocol used by the connection associated with this tunnel
+     *     cannot be determined.
+     */
+    @GET
+    @Path("protocol")
+    public ProtocolInfo getProtocol() throws GuacamoleException {
+
+        // Pull protocol name from underlying socket
+        String protocol = tunnel.getSocket().getProtocol();
+        if (protocol == null)
+            throw new GuacamoleResourceNotFoundException("Protocol of tunnel is not known/exposed.");
+
+        // If there is no such protocol defined, provide as much info as is
+        // known (just the name)
+        ProtocolInfo info = environment.getProtocol(protocol);
+        if (info == null)
+            return new ProtocolInfo(protocol);
+
+        // All protocol information for this tunnel is known
+        return info;
+
+    }
+
+    /**
      * Intercepts and returns the entire contents of a specific stream.
      *
      * @param streamIndex
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/APIUserWrapper.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/APIUserWrapper.java
index c437566..b086314 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/user/APIUserWrapper.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/APIUserWrapper.java
@@ -19,13 +19,10 @@
 
 package org.apache.guacamole.rest.user;
 
-import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleUnsupportedException;
-import org.apache.guacamole.net.auth.ActivityRecord;
 import org.apache.guacamole.net.auth.Permissions;
 import org.apache.guacamole.net.auth.RelatedObjectSet;
 import org.apache.guacamole.net.auth.User;
@@ -139,9 +136,4 @@
         return null;
     }
 
-    @Override
-    public List<? extends ActivityRecord> getHistory() throws GuacamoleException {
-        return Collections.<ActivityRecord>emptyList();
-    }
-
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java
index d7d4bdc..f31ce5d 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java
@@ -21,8 +21,6 @@
 
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
-import java.util.ArrayList;
-import java.util.List;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
@@ -33,19 +31,22 @@
 import javax.ws.rs.core.MediaType;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleSecurityException;
-import org.apache.guacamole.net.auth.ActivityRecord;
+import org.apache.guacamole.GuacamoleUnsupportedException;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.net.auth.User;
 import org.apache.guacamole.net.auth.Directory;
 import org.apache.guacamole.net.auth.UserContext;
 import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
+import org.apache.guacamole.net.auth.simple.SimpleActivityRecordSet;
 import org.apache.guacamole.rest.directory.DirectoryObjectResource;
 import org.apache.guacamole.rest.directory.DirectoryObjectTranslator;
-import org.apache.guacamole.rest.history.APIActivityRecord;
+import org.apache.guacamole.rest.history.UserHistoryResource;
 import org.apache.guacamole.rest.identifier.RelatedObjectSetResource;
 import org.apache.guacamole.rest.permission.APIPermissionSet;
 import org.apache.guacamole.rest.permission.PermissionSetResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * A REST resource which abstracts the operations available on an existing
@@ -57,6 +58,11 @@
         extends DirectoryObjectResource<User, APIUser> {
 
     /**
+     * Logger for this class.
+     */
+    private static final Logger logger = LoggerFactory.getLogger(UserResource.class);
+    
+    /**
      * The UserContext associated with the Directory which contains the User
      * exposed by this resource.
      */
@@ -110,18 +116,29 @@
      * @throws GuacamoleException
      *     If an error occurs while retrieving the user history.
      */
-    @GET
+    @SuppressWarnings("deprecation")
     @Path("history")
-    public List<APIActivityRecord> getUserHistory()
+    public UserHistoryResource getUserHistory()
             throws GuacamoleException {
 
-        // Retrieve the requested user's history
-        List<APIActivityRecord> apiRecords = new ArrayList<APIActivityRecord>();
-        for (ActivityRecord record : user.getHistory())
-            apiRecords.add(new APIActivityRecord(record));
-
-        // Return the converted history
-        return apiRecords;
+        // First try to retrieve history using the current getUserHistory() method.
+        try {
+            return new UserHistoryResource(user.getUserHistory());
+        }
+        catch (GuacamoleUnsupportedException e) {
+            logger.debug("Call to getUserHistory() is unsupported, falling back to deprecated method getHistory().", e);
+        }
+        
+        // Fall back to deprecated getHistory() method.
+        try {
+            return new UserHistoryResource(new SimpleActivityRecordSet<>(user.getHistory()));
+        }
+        catch (GuacamoleUnsupportedException e) {
+            logger.debug("Call to getHistory() is unsupported, no user history records will be returned.", e);
+        }
+        
+        // If both are unimplemented, return an empty history set.
+        return new UserHistoryResource(new SimpleActivityRecordSet<>());
 
     }
 
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index 326d868..451adee 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -732,8 +732,14 @@
         return $scope.client && $scope.client.clientState.tunnelUnstable;
     };
 
-    // Show status dialog when connection status changes
-    $scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) {
+    /**
+     * Notifies the user that the connection state has changed.
+     *
+     * @param {String} connectionState
+     *     The current connection state, as defined by
+     *     ManagedClientState.ConnectionState.
+     */
+    var notifyConnectionState = function notifyConnectionState(connectionState) {
 
         // Hide any existing status
         guacNotification.showStatus(false);
@@ -835,6 +841,105 @@
         else
             guacNotification.showStatus(false);
 
+    };
+
+    /**
+     * Prompts the user to enter additional connection parameters. If the
+     * protocol and associated parameters of the underlying connection are not
+     * yet known, this function has no effect and should be re-invoked once
+     * the parameters are known.
+     *
+     * @param {Object.<String, String>} requiredParameters
+     *     The set of all parameters requested by the server via "required"
+     *     instructions, where each object key is the name of a requested
+     *     parameter and each value is the current value entered by the user.
+     */
+    var notifyParametersRequired = function notifyParametersRequired(requiredParameters) {
+
+        /**
+         * Action which submits the current set of parameter values, requesting
+         * that the connection continue.
+         */
+        var SUBMIT_PARAMETERS = {
+            name      : "CLIENT.ACTION_CONTINUE",
+            className : "button",
+            callback  : function submitParameters() {
+                if ($scope.client) {
+                    var params = $scope.client.requiredParameters;
+                    $scope.client.requiredParameters = null;
+                    ManagedClient.sendArguments($scope.client, params);
+                }
+            }
+        };
+
+        /**
+         * Action which cancels submission of additional parameters and
+         * disconnects from the current connection.
+         */
+        var CANCEL_PARAMETER_SUBMISSION = {
+            name      : "CLIENT.ACTION_CANCEL",
+            className : "button",
+            callback  : function cancelSubmission() {
+                $scope.client.requiredParameters = null;
+                $scope.disconnect();
+            }
+        };
+
+        // Attempt to prompt for parameters only if the parameters that apply
+        // to the underlying connection are known
+        if (!$scope.client.protocol || !$scope.client.forms)
+            return;
+
+        // Hide any existing status
+        guacNotification.showStatus(false);
+
+        // Prompt for parameters
+        guacNotification.showStatus({
+            formNamespace : Protocol.getNamespace($scope.client.protocol),
+            forms : $scope.client.forms,
+            formModel : requiredParameters,
+            formSubmitCallback : SUBMIT_PARAMETERS.callback,
+            actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ]
+        });
+
+    };
+
+    /**
+     * Returns whether the given connection state allows for submission of
+     * connection parameters via "argv" instructions.
+     *
+     * @param {String} connectionState
+     *     The connection state to test, as defined by
+     *     ManagedClientState.ConnectionState.
+     *
+     * @returns {boolean}
+     *     true if the given connection state allows submission of connection
+     *     parameters via "argv" instructions, false otherwise.
+     */
+    var canSubmitParameters = function canSubmitParameters(connectionState) {
+        return (connectionState === ManagedClientState.ConnectionState.WAITING ||
+                connectionState === ManagedClientState.ConnectionState.CONNECTED);
+    };
+
+    // Show status dialog when connection status changes
+    $scope.$watchGroup([
+        'client.clientState.connectionState',
+        'client.requiredParameters',
+        'client.protocol',
+        'client.forms'
+    ], function clientStateChanged(newValues) {
+
+        var connectionState = newValues[0];
+        var requiredParameters = newValues[1];
+
+        // Prompt for parameters only if parameters can actually be submitted
+        if (requiredParameters && canSubmitParameters(connectionState))
+            notifyParametersRequired(requiredParameters);
+
+        // Otherwise, just show general connection state
+        else
+            notifyConnectionState(connectionState);
+
     });
 
     $scope.zoomIn = function zoomIn() {
diff --git a/guacamole/src/main/webapp/app/client/styles/notification.css b/guacamole/src/main/webapp/app/client/styles/notification.css
new file mode 100644
index 0000000..77a0a64
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/styles/notification.css
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+.client .notification .parameters h3,
+.client .notification .parameters .password-field .toggle-password {
+    display: none;
+}
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
index c1eccdd..12b9f57 100644
--- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js
+++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
@@ -167,6 +167,16 @@
         });
 
         /**
+         * The current state of all parameters requested by the server via
+         * "required" instructions, where each object key is the name of a
+         * requested parameter and each value is the current value entered by
+         * the user or null if no parameters are currently being requested.
+         *
+         * @type Object.<String, String>
+         */
+        this.requiredParameters = null;
+
+        /**
          * All uploaded files. As files are uploaded, their progress can be
          * observed through the elements of this array. It is intended that
          * this array be manipulated externally as needed.
@@ -376,7 +386,16 @@
                     status.code);
             });
         };
-        
+
+        // Pull protocol-specific information from tunnel once tunnel UUID is
+        // known
+        tunnel.onuuid = function tunnelAssignedUUID(uuid) {
+            tunnelService.getProtocol(uuid).then(function protocolRetrieved(protocol) {
+                managedClient.protocol = protocol.name;
+                managedClient.forms = protocol.connectionForms;
+            }, requestService.WARN);
+        };
+
         // Update connection state as tunnel state changes
         tunnel.onstatechange = function tunnelStateChanged(state) {
             $rootScope.$evalAsync(function updateTunnelState() {
@@ -578,6 +597,16 @@
             });
         };
 
+        // Handle any received prompts
+        client.onrequired = function onrequired(parameters) {
+            $rootScope.$apply(function promptUser() {
+                managedClient.requiredParameters = {};
+                angular.forEach(parameters, function populateParameter(name) {
+                    managedClient.requiredParameters[name] = '';
+                });
+            });
+        };
+
         // Manage the client display
         managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay());
 
@@ -592,14 +621,9 @@
 
         // If using a connection, pull connection name and protocol information
         if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
-            $q.all({
-                connection : connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id),
-                protocols  : schemaService.getProtocols(clientIdentifier.dataSource)
-            })
-            .then(function dataRetrieved(values) {
-                managedClient.name = managedClient.title = values.connection.name;
-                managedClient.protocol = values.connection.protocol;
-                managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
+            connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id)
+            .then(function connectionRetrieved(connection) {
+                managedClient.name = managedClient.title = connection.name;
             }, requestService.WARN);
         }
         
@@ -620,14 +644,9 @@
                 // Attempt to retrieve connection details only if the
                 // underlying connection is known
                 if (activeConnection.connectionIdentifier) {
-                    $q.all({
-                        connection : connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier),
-                        protocols  : schemaService.getProtocols(clientIdentifier.dataSource)
-                    })
-                    .then(function dataRetrieved(values) {
-                        managedClient.name = managedClient.title = values.connection.name;
-                        managedClient.protocol = values.connection.protocol;
-                        managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
+                    connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier)
+                    .then(function connectionRetrieved(connection) {
+                        managedClient.name = managedClient.title = connection.name;
                     }, requestService.WARN);
                 }
 
@@ -737,6 +756,29 @@
     };
 
     /**
+     * Sends the given connection parameter values using "argv" streams,
+     * updating the behavior of the connection in real-time if the server is
+     * expecting or requiring these parameters.
+     *
+     * @param {ManagedClient} managedClient
+     *     The ManagedClient instance associated with the active connection
+     *     being modified.
+     *
+     * @param {Object.<String, String>} values
+     *     The set of values to attempt to assign to corresponding connection
+     *     parameters, where each object key is the connection parameter being
+     *     set.
+     */
+    ManagedClient.sendArguments = function sendArguments(managedClient, values) {
+        angular.forEach(values, function sendArgument(value, name) {
+            var stream = managedClient.client.createArgumentValueStream("text/plain", name);
+            var writer = new Guacamole.StringWriter(stream);
+            writer.sendText(value);
+            writer.sendEnd();
+        });
+    };
+
+    /**
      * Retrieves the current values of all editable connection parameters as a
      * set of name/value pairs suitable for use as the model of a form which
      * edits those parameters.
diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js
index ac2fec4..9b90782 100644
--- a/guacamole/src/main/webapp/app/index/controllers/indexController.js
+++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js
@@ -101,8 +101,9 @@
     // Broadcast keydown events
     keyboard.onkeydown = function onkeydown(keysym) {
 
-        // Do not handle key events if not logged in
-        if ($scope.expectedCredentials)
+        // Do not handle key events if not logged in or if a notification is
+        // shown
+        if ($scope.expectedCredentials || guacNotification.getStatus())
             return true;
 
         // Warn of pending keydown
@@ -119,8 +120,9 @@
     // Broadcast keyup events
     keyboard.onkeyup = function onkeyup(keysym) {
 
-        // Do not handle key events if not logged in
-        if ($scope.expectedCredentials)
+        // Do not handle key events if not logged in or if a notification is
+        // shown
+        if ($scope.expectedCredentials || guacNotification.getStatus())
             return;
 
         // Warn of pending keyup
diff --git a/guacamole/src/main/webapp/app/notification/styles/notification.css b/guacamole/src/main/webapp/app/notification/styles/notification.css
index ee20e13..3d55753 100644
--- a/guacamole/src/main/webapp/app/notification/styles/notification.css
+++ b/guacamole/src/main/webapp/app/notification/styles/notification.css
@@ -100,4 +100,45 @@
 
 .notification .progress .text {
     position: relative;
-}
\ No newline at end of file
+}
+
+.notification .parameters {
+    width: 100%;
+}
+
+.notification .parameters .fields {
+    display: table;
+    width: 100%;
+}
+
+.notification .parameters .fields .labeled-field {
+    display: table-row;
+}
+
+.notification .parameters .fields .field-header,
+.notification .parameters .fields .form-field {
+    text-align: left;
+    display: table-cell;
+    padding: .125em;
+    vertical-align: top;
+}
+
+.notification .parameters .fields .field-header {
+    padding-right: 1em;
+}
+
+.notification .parameters .fields .field-header {
+    width: 0;
+}
+
+.notification .parameters .fields .form-field {
+    width: 100%;
+}
+
+.notification .parameters input[type=text],
+.notification .parameters input[type=email],
+.notification .parameters input[type=number],
+.notification .parameters input[type=password],
+.notification .parameters textarea {
+    max-width: 100%;
+}
diff --git a/guacamole/src/main/webapp/app/notification/templates/guacNotification.html b/guacamole/src/main/webapp/app/notification/templates/guacNotification.html
index 6c1cc00..b002bb0 100644
--- a/guacamole/src/main/webapp/app/notification/templates/guacNotification.html
+++ b/guacamole/src/main/webapp/app/notification/templates/guacNotification.html
@@ -1,4 +1,5 @@
-<div class="notification" ng-class="notification.className">
+<form class="notification" ng-class="notification.className"
+      ng-submit="notification.formSubmitCallback()">
 
     <!-- Notification title -->
     <div ng-show="notification.title" class="title-bar">
@@ -12,6 +13,15 @@
            translate="{{notification.text.key}}"
            translate-values="{{notification.text.variables}}"></p>
 
+        <!-- Arbitrary parameters -->
+        <div class="parameters" ng-show="notification.forms">
+            <guac-form
+                namespace="notification.formNamespace"
+                content="notification.forms"
+                model="notification.formModel"
+                model-only="true"></guac-form>
+        </div>
+
         <!-- Current progress -->
         <div class="progress" ng-show="notification.progress"><div class="bar" ng-show="progressPercent" ng-style="{'width': progressPercent + '%'}"></div><div
                 ng-show="notification.progress.text"
diff --git a/guacamole/src/main/webapp/app/notification/types/Notification.js b/guacamole/src/main/webapp/app/notification/types/Notification.js
index 921fa40..eee1601 100644
--- a/guacamole/src/main/webapp/app/notification/types/Notification.js
+++ b/guacamole/src/main/webapp/app/notification/types/Notification.js
@@ -58,6 +58,41 @@
         this.text = template.text;
 
         /**
+         * The translation namespace of the translation strings that will
+         * be generated for all fields within the notification. This namespace
+         * is absolutely required if form fields will be included in the
+         * notification.
+         *
+         * @type String
+         */
+        this.formNamespace = template.formNamespace;
+
+        /**
+         * Optional form content to display. This may be a form, an array of
+         * forms, or a simple array of fields.
+         *
+         * @type Form[]|Form|Field[]|Field
+         */
+        this.forms = template.forms;
+
+        /**
+         * The object which will receive all field values. Each field value
+         * will be assigned to the property of this object having the same
+         * name.
+         *
+         * @type Object.<String, String>
+         */
+        this.formModel = template.model;
+
+        /**
+         * The function to invoke when the form is submitted, if form fields
+         * are present within the notification.
+         *
+         * @type Function
+         */
+        this.formSubmitCallback = template.formSubmitCallback;
+
+        /**
          * An array of all actions available to the user in response to this
          * notification.
          *
diff --git a/guacamole/src/main/webapp/app/rest/services/tunnelService.js b/guacamole/src/main/webapp/app/rest/services/tunnelService.js
index 1dba527..3cfdb35 100644
--- a/guacamole/src/main/webapp/app/rest/services/tunnelService.js
+++ b/guacamole/src/main/webapp/app/rest/services/tunnelService.js
@@ -80,6 +80,36 @@
     };
 
     /**
+     * Makes a request to the REST API to retrieve the underlying protocol of
+     * the connection associated with a particular tunnel, returning a promise
+     * that provides a @link{Protocol} object if successful.
+     *
+     * @param {String} tunnel
+     *     The UUID of the tunnel associated with the Guacamole connection
+     *     whose underlying protocol is being retrieved.
+     *
+     * @returns {Promise.<Protocol>}
+     *     A promise which will resolve with a @link{Protocol} object upon
+     *     success.
+     */
+    service.getProtocol = function getProtocol(tunnel) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Retrieve the protocol details of the specified tunnel
+        return requestService({
+            method  : 'GET',
+            url     : 'api/session/tunnels/' + encodeURIComponent(tunnel)
+                        + '/protocol',
+            params  : httpParameters
+        });
+
+    };
+
+    /**
      * Retrieves the set of sharing profiles that the current user can use to
      * share the active connection of the given tunnel.
      *
diff --git a/guacamole/src/main/webapp/translations/ca.json b/guacamole/src/main/webapp/translations/ca.json
new file mode 100644
index 0000000..45e6aeb
--- /dev/null
+++ b/guacamole/src/main/webapp/translations/ca.json
@@ -0,0 +1,998 @@
+{
+    
+    "NAME" : "Catalan",
+    
+    "APP" : {
+
+        "ACTION_ACKNOWLEDGE"        : "OK",
+        "ACTION_CANCEL"             : "Cancel·lar",
+        "ACTION_CLONE"              : "Clon",
+        "ACTION_CONTINUE"           : "Continua",
+        "ACTION_DELETE"             : "Suprimeix",
+        "ACTION_DELETE_SESSIONS"    : "Mata Sessions",
+        "ACTION_DOWNLOAD"           : "Descarregar",
+        "ACTION_LOGIN"              : "Iniciar Sessió",
+        "ACTION_LOGOUT"             : "Tancar sessió",
+        "ACTION_MANAGE_CONNECTIONS" : "Connexions",
+        "ACTION_MANAGE_PREFERENCES" : "Preferències",
+        "ACTION_MANAGE_SETTINGS"    : "Configuració",
+        "ACTION_MANAGE_SESSIONS"    : "Sessions actives",
+        "ACTION_MANAGE_USERS"       : "Usuaris",
+        "ACTION_MANAGE_USER_GROUPS" : "Grups",
+        "ACTION_NAVIGATE_BACK"      : "Enrera",
+        "ACTION_NAVIGATE_HOME"      : "Inici",
+        "ACTION_SAVE"               : "Desa",
+        "ACTION_SEARCH"             : "Cerca",
+        "ACTION_SHARE"              : "Compartir",
+        "ACTION_UPDATE_PASSWORD"    : "Actualitza la contrasenya",
+        "ACTION_VIEW_HISTORY"       : "Història",
+
+        "DIALOG_HEADER_ERROR" : "Error",
+
+        "ERROR_PAGE_UNAVAILABLE"  : "S'ha produït un error i aquesta acció no es pot completar. Si el problema continua, aviseu l'administrador del sistema o comproveu els registres del vostre sistema.",
+        "ERROR_PASSWORD_BLANK"    : "La vostra contrasenya no pot estar en blanc.",
+        "ERROR_PASSWORD_MISMATCH" : "Les contrasenyes proporcionades no coincideixen.",
+        
+        "FIELD_HEADER_PASSWORD"       : "Contrasenya:",
+        "FIELD_HEADER_PASSWORD_AGAIN" : "Torna a escriure la contrasenya:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "Filtre",
+
+        "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss",
+
+        "INFO_ACTIVE_USER_COUNT" : "Actualment en ús per {USERS} {USERS, plural, one{usuari} other{usrris}}.",
+
+        "TEXT_ANONYMOUS_USER"   : "Anònim",
+        "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{segon} other{segons}}} minute{{VALUE, plural, one{minut} other{minuts}}} hour{{VALUE, plural, one{hora} other{hores}}} day{{VALUE, plural, one{dia} other{dies}}} other{}}"
+
+    },
+
+    "CLIENT" : {
+
+        "ACTION_ACKNOWLEDGE"               : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Esborra",
+        "ACTION_DISCONNECT"                : "Desconnecta",
+        "ACTION_LOGOUT"                    : "@:APP.ACTION_LOGOUT",
+        "ACTION_NAVIGATE_BACK"             : "@:APP.ACTION_NAVIGATE_BACK",
+        "ACTION_NAVIGATE_HOME"             : "@:APP.ACTION_NAVIGATE_HOME",
+        "ACTION_RECONNECT"                 : "Torneu a connectar",
+        "ACTION_SAVE_FILE"                 : "@:APP.ACTION_SAVE",
+        "ACTION_SHARE"                     : "@:APP.ACTION_SHARE",
+        "ACTION_UPLOAD_FILES"              : "Carregueu fitxers",
+
+        "DIALOG_HEADER_CONNECTING"       : "Connectant",
+        "DIALOG_HEADER_CONNECTION_ERROR" : "Error de connexió",
+        "DIALOG_HEADER_DISCONNECTED"     : "Desconnectat",
+
+        "ERROR_CLIENT_201"     : "Aquesta connexió s'ha tancat perquè el servidor està ocupat. Espereu uns minuts i torna-ho a provar.",
+        "ERROR_CLIENT_202"     : "El servidor Guacamole ha tancat la connexió perquè l'escriptori remot triga massa temps a respondre. Intenteu-ho de nou o poseu-vos en contacte amb l'administrador del vostre sistema.",
+        "ERROR_CLIENT_203"     : "El servidor d'escriptori remot ha trobat un error i ha tancat la connexió. Intenteu-ho de nou o poseu-vos en contacte amb l'administrador del vostre sistema.",
+        "ERROR_CLIENT_207"     : "El servidor d'escriptori remot actualment no es pot aconseguir. Si el problema continua, aviseu l'administrador del sistema o comproveu els registres del sistema.",
+        "ERROR_CLIENT_208"     : "Actualment el servidor d'escriptori remot no està disponible. Si el problema continua, aviseu l'administrador del sistema o comproveu els registres del vostre sistema.",
+        "ERROR_CLIENT_209"     : "El servidor d'escriptori remot ha tancat la connexió perquè entra en conflicte amb una altra connexió. Torneu-ho a provar més endavant.",
+        "ERROR_CLIENT_20A"     : "El servidor d'escriptori remot ha tancat la connexió perquè sembla que no està activat. Si no és desitjat o inesperat, aviseu l'administrador del sistema o comproveu la configuració del vostre sistema.",
+        "ERROR_CLIENT_20B"     : "El servidor d'escriptori remot ha tancat força la connexió. Si aquesta no és desitjada o inesperada, notifiqueu l'administrador del sistema o comproveu els registres del sistema.",
+        "ERROR_CLIENT_301"     : "Ha fallat la sessió. Torneu-vos a connectar i torneu-ho a provar.",
+        "ERROR_CLIENT_303"     : "El servidor d'escriptori remot ha denegat l'accés a aquesta connexió. Si necessiteu accés, sol·liciteu a l'administrador del sistema que li concedeixi accés o verifiqueu la configuració del sistema.",
+        "ERROR_CLIENT_308"     : "El servidor de Guacamole ha tancat la connexió perquè no hi ha resposta del navegador durant el temps suficient perquè sembli que es trobava desconnectat. Això és generalment causat per problemes de xarxa, com ara un senyal sense fils o simplement una velocitat de xarxa molt lenta. a la xarxa i torneu-ho a provar. ",
+        "ERROR_CLIENT_31D"     : "El servidor Guacamole denega l'accés a aquesta connexió perquè heu exhaurit el límit per a l'ús de la connexió simultània per part d'un usuari individual. Tanqueu una o més connexions i torneu-ho a provar.",
+        "ERROR_CLIENT_DEFAULT" : "S'ha produït un error intern al servidor Guacamole i s'ha acabat la connexió. Si el problema continua, aviseu l'administrador del sistema o comproveu els registres del sistema.",
+
+        "ERROR_TUNNEL_201"     : "El servidor Guacamole ha rebutjat aquest intent de connexió perquè hi ha massa connexions actives. Espereu uns minuts i torna-ho a provar.",
+        "ERROR_TUNNEL_202"     : "La connexió s'ha tancat perquè el servidor triga massa temps a respondre. Això sol ser causat per problemes de xarxa, com ara un senyal sense fils o una velocitat de xarxa lenta. Comproveu la connexió de xarxa i intenteu de nou o poseu-vos en contacte amb l'administrador del sistema. ",
+        "ERROR_TUNNEL_203"     : "El servidor ha trobat un error i ha tancat la connexió. Intenteu-ho de nou o poseu-vos en contacte amb l'administrador del vostre sistema.",
+        "ERROR_TUNNEL_204"     : "La connexió sol·licitada no existeix. Comproveu el nom de la connexió i proveu-ho de nou.",
+        "ERROR_TUNNEL_205"     : "Actualment aquesta connexió està en ús i no es permet l'accés simultani a aquesta connexió. Torneu-ho a provar més endavant.",
+        "ERROR_TUNNEL_207"     : "El servidor Guacamole no es pot accedir actualment. Comproveu la vostra xarxa i torneu-ho a provar.",
+        "ERROR_TUNNEL_208"     : "El servidor Guacamole no accepta connexions. Comproveu la vostra xarxa i proveu-la de nou.",
+        "ERROR_TUNNEL_301"     : "No teniu permís per accedir a aquesta connexió perquè no esteu registrat. Inicieu la sessió i torneu-ho a provar.",
+        "ERROR_TUNNEL_303"     : "No teniu permís per accedir a aquesta connexió. Si necessiteu accés, sol·liciteu a l'administrador del sistema que us inclogui la llista d'usuaris permesos o comproveu la vostra configuració del sistema.",
+        "ERROR_TUNNEL_308"     : "El servidor de Guacamole ha tancat la connexió perquè no hi ha hagut resposta del seu navegador durant el temps suficient com perquè sembli estar desconnectat. Això generalment és causat per problemes de xarxa, com un senyal sense fils irregular o simplement velocitats de xarxa molt lentes. comprovi la seva xarxa i intenti novament .",
+        "ERROR_TUNNEL_31D"     : "El servidor Guacamole denega l'accés a aquesta connexió perquè heu exhaurit el límit per a l'ús de la connexió simultània per part d'un usuari individual. Tanqueu una o més connexions i torneu-ho a provar.",
+        "ERROR_TUNNEL_DEFAULT" : "S'ha produït un error intern al servidor Guacamole i s'ha acabat la connexió. Si el problema continua, aviseu l'administrador del sistema o comproveu els registres del sistema.",
+
+        "ERROR_UPLOAD_100"     : "La transferència de fitxers no és compatible o no està habilitada. Poseu-vos en contacte amb l'administrador del sistema o comproveu els registres del vostre sistema.",
+        "ERROR_UPLOAD_201"     : "Actualment s'estan transferint massa fitxers. Espereu que es completin les transferències existents i, després, torneu-ho a provar.",
+        "ERROR_UPLOAD_202"     : "No es pot transferir el fitxer perquè el servidor d'escriptori remot triga massa temps a respondre. Torneu-ho a provar o poseu-vos en contacte amb l'administrador del vostre sistema.",
+        "ERROR_UPLOAD_203"     : "El servidor d'escriptori remot ha trobat un error durant la transferència. Intenteu-ho de nou o poseu-vos en contacte amb l'administrador del vostre sistema.",
+        "ERROR_UPLOAD_204"     : "La destinació per a la transferència de fitxers no existeix. Comproveu que la destinació existeix i torneu-ho a provar.",
+        "ERROR_UPLOAD_205"     : "La destinació per a la transferència de fitxers està bloquejada. Espereu qualsevol finalització de les tasques en curs i torneu-ho a provar.",
+        "ERROR_UPLOAD_301"     : "No teniu permís per carregar aquest fitxer perquè no esteu registrat. Inicieu la sessió i torneu-ho a provar.",
+        "ERROR_UPLOAD_303"     : "No teniu permís per carregar aquest fitxer. Si necessiteu accés, consulteu la configuració del sistema o consulteu-lo amb l'administrador del vostre sistema.",
+        "ERROR_UPLOAD_308"     : "La transferència de fitxers s'ha aturat. Això és habitualment causat per problemes de xarxa, com ara un senyal sense fils o una velocitat de xarxa molt lenta.",
+        "ERROR_UPLOAD_31D"     : "Actualment s'estan transferint massa fitxers. Espereu que es completin les transferències existents i, després, torneu-ho a provar.",
+        "ERROR_UPLOAD_DEFAULT" : "S'ha produït un error intern al servidor Guacamole i s'ha acabat la connexió. Si el problema continua, aviseu l'administrador del sistema o comproveu els registres del sistema.",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_CLIPBOARD"           : "Aquí apareixerà un text copiat/tallat dins de Guacamole. Els canvis al text següent afectaran el porta-retalls remot.",
+        "HELP_INPUT_METHOD_NONE"   : "No s'utilitza cap mètode d'entrada. S'accepta l'entrada de teclat des d'un teclat físic connectat.",
+        "HELP_INPUT_METHOD_OSK"    : "Mostra i accepta l'entrada del teclat integrat de Guacamole a la pantalla. El teclat a la pantalla permet escriure combinacions de tecles que, d'una altra manera, poden ser impossibles (com ara Ctrl-Alt-Del).",
+        "HELP_INPUT_METHOD_TEXT"   : "Permet escriure text i emula esdeveniments del teclat en funció del text mecanografiat. Això és necessari per a dispositius com ara telèfons mòbils que no tinguin un teclat físic.",
+        "HELP_MOUSE_MODE"          : "Determina el comportament del ratolí remot respecte als tocs.",
+        "HELP_MOUSE_MODE_ABSOLUTE" : "Toqueu per fer clic. El clic es produeix a la ubicació del toc.",
+        "HELP_MOUSE_MODE_RELATIVE" : "Arrossegueu per moure el punter del ratolí i toqueu per fer clic. El clic es produeix a la ubicació del punter.",
+        "HELP_SHARE_LINK"          : "La connexió actual s'està compartint i qualsevol usuari pot accedir amb els següents {LINKS, plural, one{enllaç} other{enllaços}}:",
+
+        "INFO_CONNECTION_SHARED" : "Aquesta connexió es comparteix ara.",
+        "INFO_NO_FILE_TRANSFERS" : "No hi ha transferències de fitxers.",
+    
+        "NAME_INPUT_METHOD_NONE"   : "Cap",
+        "NAME_INPUT_METHOD_OSK"    : "Teclat a la pantalla",
+        "NAME_INPUT_METHOD_TEXT"   : "Entrada de text",
+        "NAME_KEY_CTRL"            : "Ctrl",
+        "NAME_KEY_ALT"             : "Alt",
+        "NAME_KEY_ESC"             : "Esc",
+        "NAME_KEY_TAB"             : "Tab",
+        "NAME_MOUSE_MODE_ABSOLUTE" : "Pantalla tàctil",
+        "NAME_MOUSE_MODE_RELATIVE" : "Touchpad",
+
+        "SECTION_HEADER_CLIPBOARD"      : "Portapapers",
+        "SECTION_HEADER_DEVICES"        : "Dispositius",
+        "SECTION_HEADER_DISPLAY"        : "Pantalla",
+        "SECTION_HEADER_FILE_TRANSFERS" : "Transferències de fitxers",
+        "SECTION_HEADER_INPUT_METHOD"   : "Mètode d'entrada",
+        "SECTION_HEADER_MOUSE_MODE"     : "Mode d'emulació del ratolí",
+
+        "TEXT_ZOOM_AUTO_FIT"              : "S'adapta automàticament a la finestra del navegador",
+        "TEXT_CLIENT_STATUS_IDLE"         : "Ociós.",
+        "TEXT_CLIENT_STATUS_CONNECTING"   : "S'està connectant a Guacamole ...",
+        "TEXT_CLIENT_STATUS_DISCONNECTED" : "Heu estat desconnectats.",
+        "TEXT_CLIENT_STATUS_UNSTABLE"     : "La connexió de xarxa al servidor Guacamole sembla inestable.",
+        "TEXT_CLIENT_STATUS_WAITING"      : "Connectat a Guacamole. Esperant resposta ...",
+        "TEXT_RECONNECT_COUNTDOWN"        : "Re-conectant en {REMAINING} {REMAINING, plural, one{segon} other{segons}}...",
+        "TEXT_FILE_TRANSFER_PROGRESS"     : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
+
+        "URL_OSK_LAYOUT" : "layouts/es-es-qwerty.json"
+
+    },
+
+    "COLOR_SCHEME" : {
+
+        "ACTION_CANCEL"       : "@:APP.ACTION_CANCEL",
+        "ACTION_HIDE_DETAILS" : "Amaga",
+        "ACTION_SAVE"         : "@:APP.ACTION_SAVE",
+        "ACTION_SHOW_DETAILS" : "Mostra",
+
+        "FIELD_HEADER_BACKGROUND" : "Background",
+        "FIELD_HEADER_FOREGROUND" : "Foreground",
+
+        "FIELD_OPTION_CUSTOM" : "Personalitzat ...",
+
+        "SECTION_HEADER_DETAILS" : "Detalls:"
+
+    },
+
+    "DATA_SOURCE_DEFAULT" : {
+        "NAME" : "Default (XML)"
+    },
+
+    "FORM" : {
+
+        "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD",
+        "FIELD_PLACEHOLDER_TIME" : "HH:MM:SS",
+
+        "HELP_SHOW_PASSWORD" : "Feu clic per mostrar la contrasenya",
+        "HELP_HIDE_PASSWORD" : "Feu clic per ocultar la contrasenya"
+
+    },
+
+    "HOME" : {
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
+
+        "INFO_NO_RECENT_CONNECTIONS" : "No hi ha connexions recents.",
+        
+        "PASSWORD_CHANGED" : "S'ha canviat la contrasenya.",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"    : "Totes les connexions",
+        "SECTION_HEADER_RECENT_CONNECTIONS" : "Connexions recents"
+
+    },
+
+    "LIST" : {
+
+        "TEXT_ANONYMOUS_USER" : "Anònim"
+
+    },
+
+    "LOGIN": {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CONTINUE"    : "@:APP.ACTION_CONTINUE",
+        "ACTION_LOGIN"       : "@:APP.ACTION_LOGIN",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_INVALID_LOGIN" : "Accés incorrecte",
+
+        "FIELD_HEADER_USERNAME" : "Nom d'usuari",
+        "FIELD_HEADER_PASSWORD" : "Contrasenya"
+
+    },
+
+    "MANAGE_CONNECTION" : {
+
+        "ACTION_ACKNOWLEDGE"          : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"               : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"                : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"               : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"                 : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Suprimeix la connexió",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_LOCATION" : "Ubicació:",
+        "FIELD_HEADER_NAME"     : "Nom:",
+        "FIELD_HEADER_PROTOCOL" : "Protocol:",
+
+        "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "INFO_CONNECTION_DURATION_UNKNOWN" : "--",
+        "INFO_CONNECTION_ACTIVE_NOW"       : "Actiu ara",
+        "INFO_CONNECTION_NOT_USED"         : "Aquesta connexió encara no s'ha utilitzat.",
+
+        "SECTION_HEADER_EDIT_CONNECTION" : "Edita la connexió",
+        "SECTION_HEADER_HISTORY"         : "Historial d'ús",
+        "SECTION_HEADER_PARAMETERS"      : "Paràmetres",
+
+        "TABLE_HEADER_HISTORY_USERNAME"   : "Nom d'usuari",
+        "TABLE_HEADER_HISTORY_START"      : "L'hora d'inici",
+        "TABLE_HEADER_HISTORY_DURATION"   : "Durada",
+        "TABLE_HEADER_HISTORY_REMOTEHOST" : "Servidor Remot",
+
+        "TEXT_CONFIRM_DELETE"   : "No es poden restaurar les connexions un cop s'hagin suprimit. Esteu segur que voleu suprimir aquesta connexió?",
+        "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION"
+
+    },
+
+    "MANAGE_CONNECTION_GROUP" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"        : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"         : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"        : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"          : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Suprimeix el grup de connexió",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_LOCATION" : "Ubicació:",
+        "FIELD_HEADER_NAME"     : "Nom:",
+        "FIELD_HEADER_TYPE"     : "Tipus:",
+
+        "NAME_TYPE_BALANCING"       : "Balanceig",
+        "NAME_TYPE_ORGANIZATIONAL"  : "Organitzatiu",
+
+        "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Edita el grup de connexió",
+
+        "TEXT_CONFIRM_DELETE" : "No es poden restaurar els grups de connexió un cop suprimits. Esteu segur que voleu suprimir aquest grup de connexió?"
+
+    },
+
+    "MANAGE_SHARING_PROFILE" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"       : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"      : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"        : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Eliminar el perfil compartit",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_NAME"               : "Nom:",
+        "FIELD_HEADER_PRIMARY_CONNECTION" : "Connexió primària:",
+
+        "SECTION_HEADER_EDIT_SHARING_PROFILE" : "Edita el perfil de compartir",
+        "SECTION_HEADER_PARAMETERS"           : "Paràmetres",
+
+        "TEXT_CONFIRM_DELETE" : "Els perfils per compartir no es poden restaurar un cop s'hagin suprimit. Esteu segur que voleu suprimir aquest perfil compartit?"
+
+    },
+
+    "MANAGE_USER" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"        : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"         : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"        : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"          : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Eliminar l'usuari",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+
+        "FIELD_HEADER_ADMINISTER_SYSTEM"             : "Administrar sistema:",
+        "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "Canvieu la contrasenya pròpia:",
+        "FIELD_HEADER_CREATE_NEW_USERS"              : "Crea usuaris nous:",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "Creeu grups d'usuaris nous:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "Crear noves connexions:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "Crear grups de connexió nous:",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "Crea nous perfils per compartir:",
+        "FIELD_HEADER_PASSWORD"                      : "@:APP.FIELD_HEADER_PASSWORD",
+        "FIELD_HEADER_PASSWORD_AGAIN"                : "@:APP.FIELD_HEADER_PASSWORD_AGAIN",
+        "FIELD_HEADER_USERNAME"                      : "Nom d'usuari:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS" : "Actualment, aquest usuari no pertany a cap grup. Amplieu aquesta secció per afegir grups.",
+
+        "INFO_READ_ONLY"                : "Ho sentim, però aquest compte d'usuari no es pot modificar.",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "No hi ha grups disponibles.",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "Totes les connexions",
+        "SECTION_HEADER_CONNECTIONS"         : "Connexions",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "Connexions actuals",
+        "SECTION_HEADER_EDIT_USER"           : "Edita l'usuari",
+        "SECTION_HEADER_PERMISSIONS"         : "Permisos",
+        "SECTION_HEADER_USER_GROUPS"         : "Grups",
+
+        "TEXT_CONFIRM_DELETE" : "Els usuaris no es poden restaurar un cop se'ls ha suprimit. Esteu segur que voleu suprimir aquest usuari?"
+
+    },
+
+    "MANAGE_USER_GROUP" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"        : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"         : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"        : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"          : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Suprimeix el grup",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_ADMINISTER_SYSTEM"             : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM",
+        "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD",
+        "FIELD_HEADER_CREATE_NEW_USERS"              : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES",
+        "FIELD_HEADER_USER_GROUP_NAME"               : "Nom del grup:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS"        : "Actualment aquest grup no pertany a cap grup. Amplieu aquesta secció per afegir grups.",
+        "HELP_NO_MEMBER_USER_GROUPS" : "Actualment aquest grup no conté cap grup. Amplieu aquesta secció per afegir grups.",
+        "HELP_NO_MEMBER_USERS"       : "Actualment aquest grup no conté cap usuari. Amplieu aquesta secció per afegir-ne usuaris.",
+
+        "INFO_READ_ONLY"                : "Ho sentim, però no es pot editar aquest grup.",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE",
+        "INFO_NO_USERS_AVAILABLE"       : "No hi ha usuaris disponibles.",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS",
+        "SECTION_HEADER_CONNECTIONS"         : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS",
+        "SECTION_HEADER_EDIT_USER_GROUP"     : "Edita el grup",
+        "SECTION_HEADER_MEMBER_USERS"        : "Usuaris membres",
+        "SECTION_HEADER_MEMBER_USER_GROUPS"  : "Grups membres",
+        "SECTION_HEADER_PERMISSIONS"         : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS",
+        "SECTION_HEADER_USER_GROUPS"         : "Grups pare",
+
+        "TEXT_CONFIRM_DELETE" : "No es poden restaurar els grups després que se'ls hagi suprimit. Esteu segur que voleu suprimir aquest grup?"
+
+    },
+
+    "PROTOCOL_KUBERNETES" : {
+
+        "FIELD_HEADER_BACKSPACE"       : "La tecla de retrocés envia:",
+        "FIELD_HEADER_CA_CERT"         : "Certificat d'autoritat de certificació:",
+        "FIELD_HEADER_CLIENT_CERT"     : "Certificat de client:",
+        "FIELD_HEADER_CLIENT_KEY"      : "Clau del client:",
+        "FIELD_HEADER_COLOR_SCHEME"    : "Esquema de colors:",
+        "FIELD_HEADER_CONTAINER"       : "Nom del contenidor:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH"  : "Crea automàticament la ruta de gravació:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Crea automàticament la ruta de typescript:",
+        "FIELD_HEADER_FONT_NAME"       : "Nom del tipus de lletra:",
+        "FIELD_HEADER_FONT_SIZE"       : "Mida de la lletra:",
+        "FIELD_HEADER_HOSTNAME"        : "Nom de l'amfitrió:",
+        "FIELD_HEADER_IGNORE_CERT"     : "Ignora el certificat del servidor:",
+        "FIELD_HEADER_NAMESPACE"       : "Espai de noms:",
+        "FIELD_HEADER_POD"             : "Nom del pod:",
+        "FIELD_HEADER_PORT"            : "Port:",
+        "FIELD_HEADER_READ_ONLY"       : "Només Lectura:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclou el ratolí:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclou els gràfics/fluxos:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Inclou els esdeveniments de teclat:",
+        "FIELD_HEADER_RECORDING_NAME"  : "Nom de la gravació:",
+        "FIELD_HEADER_RECORDING_PATH"  : "Ruta de gravació:",
+        "FIELD_HEADER_SCROLLBACK"      : "Mida màxima de desplaçament:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom Typescript:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "Ruta Typescript :",
+        "FIELD_HEADER_USE_SSL"         : "Usa SSL/TLS",
+
+        "FIELD_OPTION_BACKSPACE_8"     : "Espai enrere (Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "Eliminar (Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Negre sobre blanc",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "Gris sobre negre",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Verd sobre negre",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Blanc sobre negre",
+
+        "FIELD_OPTION_FONT_SIZE_8"     : "8",
+        "FIELD_OPTION_FONT_SIZE_9"     : "9",
+        "FIELD_OPTION_FONT_SIZE_10"    : "10",
+        "FIELD_OPTION_FONT_SIZE_11"    : "11",
+        "FIELD_OPTION_FONT_SIZE_12"    : "12",
+        "FIELD_OPTION_FONT_SIZE_14"    : "14",
+        "FIELD_OPTION_FONT_SIZE_18"    : "18",
+        "FIELD_OPTION_FONT_SIZE_24"    : "24",
+        "FIELD_OPTION_FONT_SIZE_30"    : "30",
+        "FIELD_OPTION_FONT_SIZE_36"    : "36",
+        "FIELD_OPTION_FONT_SIZE_48"    : "48",
+        "FIELD_OPTION_FONT_SIZE_60"    : "60",
+        "FIELD_OPTION_FONT_SIZE_72"    : "72",
+        "FIELD_OPTION_FONT_SIZE_96"    : "96",
+
+        "NAME" : "Kubernetes",
+
+        "SECTION_HEADER_AUTHENTICATION" : "Autenticació",
+        "SECTION_HEADER_BEHAVIOR"       : "Comportament del terminal",
+        "SECTION_HEADER_CONTAINER"      : "Contenidor",
+        "SECTION_HEADER_DISPLAY"        : "Pantalla",
+        "SECTION_HEADER_RECORDING"      : "Gravació de la pantalla",
+        "SECTION_HEADER_TYPESCRIPT"     : "Typescript (registre de sessió de text)",
+        "SECTION_HEADER_NETWORK"        : "Xarxa"
+
+    },
+
+    "PROTOCOL_RDP" : {
+
+        "FIELD_HEADER_CLIENT_NAME"     : "Nom del client:",
+        "FIELD_HEADER_COLOR_DEPTH"     : "Profunditat del color:",
+        "FIELD_HEADER_CONSOLE"         : "Administrador de la consola:",
+        "FIELD_HEADER_CONSOLE_AUDIO"   : "Suport de l'àudio a la consola:",
+        "FIELD_HEADER_CREATE_DRIVE_PATH" : "Crea automàticament la unitat:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "Crea automàticament la ruta de gravació:",
+        "FIELD_HEADER_DISABLE_AUDIO"   : "Desactiva l'àudio:",
+        "FIELD_HEADER_DISABLE_AUTH"    : "Desactiva l'autenticació:",
+        "FIELD_HEADER_DISABLE_COPY"    : "Desactiva la còpia des de l'escriptori remot:",
+        "FIELD_HEADER_DISABLE_DOWNLOAD" : "Desactiva la baixada de fitxers:",
+        "FIELD_HEADER_DISABLE_PASTE"   : "Desactiva l’enganxament del client:",
+        "FIELD_HEADER_DISABLE_UPLOAD"   : "Desactiva la càrrega de fitxers:",
+        "FIELD_HEADER_DOMAIN"          : "Domini:",
+        "FIELD_HEADER_DPI"             : "Resolució (DPI):",
+        "FIELD_HEADER_DRIVE_NAME"      : "Nom de la unitat:",
+        "FIELD_HEADER_DRIVE_PATH"      : "Ruta de la unitat:",
+        "FIELD_HEADER_ENABLE_AUDIO_INPUT"         : "Habilita l'entrada d'àudio (micròfon):",
+        "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Habilita la composició d'escriptori (Aero):",
+        "FIELD_HEADER_ENABLE_DRIVE"               : "Habilita la unitat:",
+        "FIELD_HEADER_ENABLE_FONT_SMOOTHING"      : "Habilita el suavització de tipus de lletra (ClearType):",
+        "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG"    : "Habilita l'arrossegament de la finestra completa:",
+        "FIELD_HEADER_ENABLE_MENU_ANIMATIONS"     : "Habilita animacions de menús:",
+        "FIELD_HEADER_DISABLE_BITMAP_CACHING"     : "Desactiva la memòria cau de mapa de bits:",
+        "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING"  : "Desactiva la memòria cau fora de pantalla:",
+        "FIELD_HEADER_DISABLE_GLYPH_CACHING"      : "Desactiva la memòria cau del glif:",
+        "FIELD_HEADER_ENABLE_PRINTING"            : "Habilita la impressió:",
+        "FIELD_HEADER_ENABLE_SFTP"     : "Activa SFTP:",
+        "FIELD_HEADER_ENABLE_THEMING"             : "Activa temes:",
+        "FIELD_HEADER_ENABLE_WALLPAPER"           : "Habilita el fons de pantalla:",
+        "FIELD_HEADER_GATEWAY_DOMAIN"   : "Domini:",
+        "FIELD_HEADER_GATEWAY_HOSTNAME" : "Nom de l'amfitrió:",
+        "FIELD_HEADER_GATEWAY_PASSWORD" : "Contrasenya:",
+        "FIELD_HEADER_GATEWAY_PORT"     : "Port:",
+        "FIELD_HEADER_GATEWAY_USERNAME" : "Nom d'usuari:",
+        "FIELD_HEADER_HEIGHT"          : "Alçada:",
+        "FIELD_HEADER_HOSTNAME"        : "Nom de l'amfitrió:",
+        "FIELD_HEADER_IGNORE_CERT"     : "Ignora el certificat del servidor:",
+        "FIELD_HEADER_INITIAL_PROGRAM" : "Programa inicial:",
+        "FIELD_HEADER_LOAD_BALANCE_INFO" : "Informació del balanç de càrrega/cookie:",
+        "FIELD_HEADER_PASSWORD"        : "Contrasenya:",
+        "FIELD_HEADER_PORT"            : "Port:",
+        "FIELD_HEADER_PRINTER_NAME"    : "Nom de la impressora redirigit:",
+        "FIELD_HEADER_PRECONNECTION_BLOB" : "BLOB de preconnexió BLOB (VM ID):",
+        "FIELD_HEADER_PRECONNECTION_ID"   : "Identificador de font RDP: ",
+        "FIELD_HEADER_READ_ONLY"      : "Només Lectura:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclou el ratolí:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclou els gràfics/fluxos:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Inclou els esdeveniments de teclat:",
+        "FIELD_HEADER_RECORDING_NAME" : "Nom de la gravació:",
+        "FIELD_HEADER_RECORDING_PATH" : "Ruta de gravaciIELD_HEADER_DISABLE_COPYó:",
+        "FIELD_HEADER_RESIZE_METHOD" : "Mètode de redimensionament:",
+        "FIELD_HEADER_REMOTE_APP_ARGS" : "Paràmetres:",
+        "FIELD_HEADER_REMOTE_APP_DIR"  : "Directori de treball:",
+        "FIELD_HEADER_REMOTE_APP"      : "Programa:",
+        "FIELD_HEADER_SECURITY"        : "Mode de seguretat:",
+        "FIELD_HEADER_SERVER_LAYOUT"   : "Disposició del teclat:",
+        "FIELD_HEADER_SFTP_DIRECTORY"             : "Directori de càrrega per defecte:",
+        "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD"      : "Desactiva la baixada de fitxers:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "Clau d'amfitrió pública (Base64):",
+        "FIELD_HEADER_SFTP_HOSTNAME"              : "Nom de l'amfitrió:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Interval de manteniment SFTP:",
+        "FIELD_HEADER_SFTP_PASSPHRASE"            : "Frase d'accés:",
+        "FIELD_HEADER_SFTP_PASSWORD"              : "Contrasenya:",
+        "FIELD_HEADER_SFTP_PORT"                  : "Port:",
+        "FIELD_HEADER_SFTP_PRIVATE_KEY"           : "Clau privada:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "Directori arrel del navegador de fitxers:",
+        "FIELD_HEADER_SFTP_DISABLE_UPLOAD"        : "Desactiva la càrrega de fitxers:",
+        "FIELD_HEADER_SFTP_USERNAME"              : "Nom d'usuari:",
+        "FIELD_HEADER_STATIC_CHANNELS" : "Noms de canals estàtics:",
+        "FIELD_HEADER_TIMEZONE"        : "Fus horari:",
+        "FIELD_HEADER_USERNAME"        : "Nom d'usuari:",
+        "FIELD_HEADER_WIDTH"           : "Amplada:",
+        "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adreça de difusió del paquet WoL:",
+        "FIELD_HEADER_WOL_MAC_ADDR"       : "Adreça MAC de l'amfitrió remot:",
+        "FIELD_HEADER_WOL_SEND_PACKET"    : "Enviar paquet WoL:",
+        "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'espera de l'arrencada d'amfitrió:",
+
+
+        "FIELD_OPTION_COLOR_DEPTH_16"    : "Color baix (16 bits)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "Veritable color (24 bits)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "Veritable color (32 bits)",
+        "FIELD_OPTION_COLOR_DEPTH_8"     : "256 colors",
+
+
+        "FIELD_OPTION_RESIZE_METHOD_DISPLAY_UPDATE" : "\"Actualizació de pantalla\" canal virtual (RDP 8.1+)",
+
+        "FIELD_OPTION_RESIZE_METHOD_RECONNECT"      : "Torna a connectar",
+
+        "FIELD_OPTION_SECURITY_ANY"   : "Qualsevol",
+
+        "FIELD_OPTION_SECURITY_NLA"   : "NLA (Network Level Authentication)",
+        "FIELD_OPTION_SECURITY_RDP"   : "RDP encryption",
+        "FIELD_OPTION_SECURITY_TLS"   : "TLS encryption",
+        "FIELD_OPTION_SECURITY_VMCONNECT" : "Hyper-V / VMConnect",
+
+        "FIELD_OPTION_SERVER_LAYOUT_DE_CH_QWERTZ" : "Suís alemany (Qwertz)",
+        "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "Alemany (Qwertz)",
+
+        "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "Anglès del Regne Unit (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "Anglès dels Estats Units (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_ES_ES_QWERTY" : "Espanyol (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Latino Americano (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE"     : "Unicode",
+        "FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Belgian French (Azerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Suís francès (Qwertz)",
+        "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Francès (Azerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Hungarian (Qwertz)",        
+        "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italià (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japonès (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portuguès brasiler (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Suec (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Danès (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Q-Turc (Qwerty)",
+
+        "NAME" : "RDP",
+
+        "SECTION_HEADER_AUTHENTICATION"     : "Autenticació",
+        "SECTION_HEADER_BASIC_PARAMETERS"   : "Configuració bàsica",
+        "SECTION_HEADER_CLIPBOARD"          : "Portapapers",
+        "SECTION_HEADER_DEVICE_REDIRECTION" : "Redirecció del dispositiu",
+        "SECTION_HEADER_DISPLAY"            : "Pantalla",
+        "SECTION_HEADER_GATEWAY"            : "Passarel·la d'escriptori remota",
+        "SECTION_HEADER_LOAD_BALANCING"     : "Equilibri de càrrega",
+        "SECTION_HEADER_NETWORK"            : "Xarxa",
+        "SECTION_HEADER_PERFORMANCE"        : "Rendiment",
+        "SECTION_HEADER_PRECONNECTION_PDU"  : "Pre connexió PDU / Hyper-V",
+        "SECTION_HEADER_RECORDING"          : "Gravació de la pantalla",
+        "SECTION_HEADER_REMOTEAPP"          : "RemoteApp",
+        "SECTION_HEADER_SFTP"               : "SFTP",
+        "SECTION_HEADER_WOL"                : "Wake-on-LAN (WoL)"
+
+    },
+
+    "PROTOCOL_SSH" : {
+
+        "FIELD_HEADER_BACKSPACE"    : "La tecla de retrocés envia:",
+        "FIELD_HEADER_COLOR_SCHEME" : "Esquema de colors:",
+        "FIELD_HEADER_COMMAND"      : "Executa l'ordre:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "Crea automàticament la ruta de gravació:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Creeu automàticament la ruta de mecanografia:",
+        "FIELD_HEADER_DISABLE_COPY"  : "Desactiva la còpia des del terminal:",
+        "FIELD_HEADER_DISABLE_PASTE" : "Desactiva l’enganxament del client:",
+        "FIELD_HEADER_FONT_NAME"     : "Nom del tipus de lletra:",
+        "FIELD_HEADER_FONT_SIZE"     : "Mida de la lletra:",
+        "FIELD_HEADER_ENABLE_SFTP"   : "Activa SFTP:",
+        "FIELD_HEADER_HOST_KEY"      : "Clau d'amfitrió pública (Base64):",
+        "FIELD_HEADER_HOSTNAME"      : "Nom de l'amfitrió:",
+        "FIELD_HEADER_LOCALE"        : "Language/Locale ($LANG):",
+        "FIELD_HEADER_USERNAME"      : "Nom d'usuari:",
+        "FIELD_HEADER_PASSWORD"      : "Contrasenya:",
+        "FIELD_HEADER_PASSPHRASE"    : "Frase d'accés:",
+        "FIELD_HEADER_PORT"          : "Port:",
+        "FIELD_HEADER_PRIVATE_KEY"   : "Clau privada:",
+        "FIELD_HEADER_SCROLLBACK"    : "Mida màxima de desplaçament:",
+        "FIELD_HEADER_READ_ONLY"     : "Només lectura:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclou el ratolí:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclou els gràfics/fluxos:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Inclou els esdeveniments de teclat:",
+        "FIELD_HEADER_RECORDING_NAME" : "Nom de la gravació:",
+        "FIELD_HEADER_RECORDING_PATH" : "Ruta de gravació:",
+        "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "Interval de 'keepalive' del servidor:",
+        "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Desactiva la baixada de fitxers:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"   : "Directori arrel del navegador de fitxers:",
+        "FIELD_HEADER_SFTP_DISABLE_UPLOAD"   : "Desactiva la càrrega de fitxers:",
+        "FIELD_HEADER_TERMINAL_TYPE"   : "Tipus de terminal:",
+        "FIELD_HEADER_TIMEZONE"        : "Zona horària ($TZ):",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom Typescript:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "Ruta Typescript:",
+        "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adreça de difusió del paquet WoL:",
+        "FIELD_HEADER_WOL_MAC_ADDR"       : "Adreça MAC de l'amfitrió remot:",
+        "FIELD_HEADER_WOL_SEND_PACKET"    : "Enviar paquet WoL:",
+        "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'espera de l'arrencada d'amfitrió:",
+
+
+        "FIELD_OPTION_BACKSPACE_8"     : "Backspace (Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "Delete (Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Negre sobre blanc",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "Gris sobre negre",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Verd sobre negre",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Blanc sobre negre",
+
+        "FIELD_OPTION_FONT_SIZE_8"     : "8",
+        "FIELD_OPTION_FONT_SIZE_9"     : "9",
+        "FIELD_OPTION_FONT_SIZE_10"    : "10",
+        "FIELD_OPTION_FONT_SIZE_11"    : "11",
+        "FIELD_OPTION_FONT_SIZE_12"    : "12",
+        "FIELD_OPTION_FONT_SIZE_14"    : "14",
+        "FIELD_OPTION_FONT_SIZE_18"    : "18",
+        "FIELD_OPTION_FONT_SIZE_24"    : "24",
+        "FIELD_OPTION_FONT_SIZE_30"    : "30",
+        "FIELD_OPTION_FONT_SIZE_36"    : "36",
+        "FIELD_OPTION_FONT_SIZE_48"    : "48",
+        "FIELD_OPTION_FONT_SIZE_60"    : "60",
+        "FIELD_OPTION_FONT_SIZE_72"    : "72",
+        "FIELD_OPTION_FONT_SIZE_96"    : "96",
+
+
+        "FIELD_OPTION_TERMINAL_TYPE_ANSI"           : "ansi",
+
+        "FIELD_OPTION_TERMINAL_TYPE_LINUX"          : "linux",
+        "FIELD_OPTION_TERMINAL_TYPE_VT100"          : "vt100",
+        "FIELD_OPTION_TERMINAL_TYPE_VT220"          : "vt220",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM"          : "xterm",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM_256COLOR" : "xterm-256color",
+
+        "NAME" : "SSH",
+
+        "SECTION_HEADER_AUTHENTICATION" : "Autenticació",
+        "SECTION_HEADER_BEHAVIOR"       : "Comportament del terminal",
+        "SECTION_HEADER_CLIPBOARD"      : "Portapapers",
+        "SECTION_HEADER_DISPLAY"        : "Pantalla",
+        "SECTION_HEADER_NETWORK"        : "Xarxa",
+        "SECTION_HEADER_RECORDING"      : "Gravació de la pantalla",
+        "SECTION_HEADER_SESSION"        : "Sessió / Entorn",
+        "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Gravació de sessió de text)",
+        "SECTION_HEADER_SFTP"           : "SFTP",
+        "SECTION_HEADER_WOL"            : "Wake-on-LAN (WoL)"
+
+    },
+
+    "PROTOCOL_TELNET" : {
+
+        "FIELD_HEADER_BACKSPACE"      : "Backspace envia tecles:",
+        "FIELD_HEADER_COLOR_SCHEME"   : "Esquema de colors:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "Crea automàticament la ruta de gravació:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Crea automàticament la ruta del typescript:",
+        "FIELD_HEADER_DISABLE_COPY"   : "Desactiva la còpia des del terminal:",
+        "FIELD_HEADER_DISABLE_PASTE"  : "Desactiva l’enganxament del client:",
+        "FIELD_HEADER_FONT_NAME"      : "Nom del tipus de lletra:",
+        "FIELD_HEADER_FONT_SIZE"      : "Mida de la lletra:",
+        "FIELD_HEADER_HOSTNAME"       : "Nom de l'amfitrió:",
+        "FIELD_HEADER_LOGIN_FAILURE_REGEX" : "Expressió regular de fallida d'inici de sessió:",
+        "FIELD_HEADER_LOGIN_SUCCESS_REGEX" : "Expressió regular d'exit d'inici de sessió:",
+        "FIELD_HEADER_USERNAME"       : "Nom d'usuari:",
+        "FIELD_HEADER_USERNAME_REGEX" : "Expressió regular del nom d'usuari:",
+        "FIELD_HEADER_PASSWORD"       : "Contrasenya:",
+        "FIELD_HEADER_PASSWORD_REGEX" : "Expressió regular de contrasenya:",
+        "FIELD_HEADER_PORT"           : "Port:",
+        "FIELD_HEADER_READ_ONLY"      : "Només lectura:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclou el ratolí:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclou els gràfics/fluxos:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Inclou els esdeveniments de teclat:",
+        "FIELD_HEADER_RECORDING_NAME" : "Nom de la gravació:",
+        "FIELD_HEADER_RECORDING_PATH" : "Ruta de gravació:",
+        "FIELD_HEADER_SCROLLBACK"     : "Mida màxima de desplaçament:",
+        "FIELD_HEADER_TERMINAL_TYPE"   : "Tipus de terminal:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom de Typescript:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "Ruta de Typescript:",
+        "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adreça de difusió del paquet WoL:",
+        "FIELD_HEADER_WOL_MAC_ADDR"       : "Adreça MAC de l'amfitrió remot:",
+        "FIELD_HEADER_WOL_SEND_PACKET"    : "Enviar paquet WoL:",
+        "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'espera de l'arrencada d'amfitrió:",
+
+
+        "FIELD_OPTION_BACKSPACE_8"     : "Backspace (Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "Delete (Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Negre sobre blanc",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "Gris sobre negre",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Verd sobre negre",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Blanc sobre negre",
+
+        "FIELD_OPTION_FONT_SIZE_8"     : "8",
+        "FIELD_OPTION_FONT_SIZE_9"     : "9",
+        "FIELD_OPTION_FONT_SIZE_10"    : "10",
+        "FIELD_OPTION_FONT_SIZE_11"    : "11",
+        "FIELD_OPTION_FONT_SIZE_12"    : "12",
+        "FIELD_OPTION_FONT_SIZE_14"    : "14",
+        "FIELD_OPTION_FONT_SIZE_18"    : "18",
+        "FIELD_OPTION_FONT_SIZE_24"    : "24",
+        "FIELD_OPTION_FONT_SIZE_30"    : "30",
+        "FIELD_OPTION_FONT_SIZE_36"    : "36",
+        "FIELD_OPTION_FONT_SIZE_48"    : "48",
+        "FIELD_OPTION_FONT_SIZE_60"    : "60",
+        "FIELD_OPTION_FONT_SIZE_72"    : "72",
+        "FIELD_OPTION_FONT_SIZE_96"    : "96",
+
+
+        "FIELD_OPTION_TERMINAL_TYPE_ANSI"           : "ansi",
+
+        "FIELD_OPTION_TERMINAL_TYPE_LINUX"          : "linux",
+        "FIELD_OPTION_TERMINAL_TYPE_VT100"          : "vt100",
+        "FIELD_OPTION_TERMINAL_TYPE_VT220"          : "vt220",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM"          : "xterm",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM_256COLOR" : "xterm-256color",
+
+        "NAME" : "Telnet",
+
+        "SECTION_HEADER_AUTHENTICATION" : "Autenticació",
+        "SECTION_HEADER_BEHAVIOR"       : "Comportament del terminal",
+        "SECTION_HEADER_CLIPBOARD"      : "Portapapers",
+        "SECTION_HEADER_DISPLAY"        : "Pantalla",
+        "SECTION_HEADER_RECORDING"      : "Gravació de la pantalla",
+        "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Gravació de sessió de text)",
+        "SECTION_HEADER_NETWORK"        : "Xarxa",
+        "SECTION_HEADER_WOL"            : "Wake-on-LAN (WoL)"
+
+    },
+
+    "PROTOCOL_VNC" : {
+
+        "FIELD_HEADER_AUDIO_SERVERNAME" : "Nom del servidor d'àudio:",
+        "FIELD_HEADER_CLIPBOARD_ENCODING" : "Codificació:",
+        "FIELD_HEADER_COLOR_DEPTH"      : "Profunditat del color:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "Crea automàticament la ruta de gravació:",
+        "FIELD_HEADER_CURSOR"           : "Cursor:",
+        "FIELD_HEADER_DEST_HOST"        : "Amfitrió de destinació:",
+        "FIELD_HEADER_DEST_PORT"        : "Port de destinació:",
+        "FIELD_HEADER_DISABLE_COPY"     : "Desactiva la còpia des de l'escriptori remot:",
+        "FIELD_HEADER_DISABLE_PASTE"    : "Desactiva l’enganxament del client:",
+        "FIELD_HEADER_ENABLE_AUDIO"     : "Habilita l'àudio:",
+        "FIELD_HEADER_ENABLE_SFTP"      : "Activa SFTP:",
+        "FIELD_HEADER_HOSTNAME"         : "Nom de l'amfitrió:",
+        "FIELD_HEADER_USERNAME"         : "Nom d'usuari:",
+        "FIELD_HEADER_PASSWORD"         : "Contrasenya:",
+        "FIELD_HEADER_PORT"             : "Port:",
+        "FIELD_HEADER_READ_ONLY"        : "Només lectura:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclou el ratolí:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclou els gràfics/fluxos:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Inclou els esdeveniments de teclat:",
+        "FIELD_HEADER_RECORDING_NAME" : "Nom de la gravació:",
+        "FIELD_HEADER_RECORDING_PATH" : "Ruta de gravació:",
+        "FIELD_HEADER_SFTP_DIRECTORY"             : "Directori de càrrega per defecte:",
+        "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD"      : "Desactiva la baixada de fitxers:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "Clau d'amfitrió pública (Base64):",
+        "FIELD_HEADER_SFTP_HOSTNAME"              : "Nom de l'amfitrió:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Interval de keepalive SFTP:",
+        "FIELD_HEADER_SFTP_PASSPHRASE"            : "Frase d'accés:",
+        "FIELD_HEADER_SFTP_PASSWORD"              : "Contrasenya:",
+        "FIELD_HEADER_SFTP_PORT"                  : "Port:",
+        "FIELD_HEADER_SFTP_PRIVATE_KEY"           : "Clau privada:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "Directori arrel del navegador de fitxers:",
+        "FIELD_HEADER_SFTP_DISABLE_UPLOAD"        : "Desactiva la càrrega de fitxers:",
+        "FIELD_HEADER_SFTP_USERNAME"              : "Nom d'usuari:",
+        "FIELD_HEADER_SWAP_RED_BLUE"    : "Canvia components vermells/blaus:",
+        "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adreça de difusió del paquet WoL:",
+        "FIELD_HEADER_WOL_MAC_ADDR"       : "Adreça MAC de l'amfitrió remot:",
+        "FIELD_HEADER_WOL_SEND_PACKET"    : "Enviar paquet WoL:",
+        "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'espera de l'arrencada d'amfitrió:",
+
+        "FIELD_OPTION_COLOR_DEPTH_8"     : "256 colors",
+        "FIELD_OPTION_COLOR_DEPTH_16"    : "Color baix (16 bits)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "Veritable color (24 bits)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "Veritable color (32 bits)",
+
+        "FIELD_OPTION_CURSOR_LOCAL"  : "Local",
+        "FIELD_OPTION_CURSOR_REMOTE" : "Remot",
+
+        "FIELD_OPTION_CLIPBOARD_ENCODING_CP1252"    : "CP1252",
+        "FIELD_OPTION_CLIPBOARD_ENCODING_ISO8859_1" : "ISO 8859-1",
+        "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_8"     : "UTF-8",
+        "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_16"    : "UTF-16",
+
+        "NAME" : "VNC",
+
+        "SECTION_HEADER_AUDIO"          : "Àudio",
+        "SECTION_HEADER_AUTHENTICATION" : "Autenticació",
+        "SECTION_HEADER_CLIPBOARD"      : "Portapapers",
+        "SECTION_HEADER_DISPLAY"        : "Pantalla",
+        "SECTION_HEADER_NETWORK"        : "Xarxa",
+        "SECTION_HEADER_RECORDING"      : "Gravació de la pantalla",
+        "SECTION_HEADER_REPEATER"       : "Repetidor VNC",
+        "SECTION_HEADER_SFTP"           : "SFTP",
+        "SECTION_HEADER_WOL"            : "Wake-on-LAN (WoL)"
+
+    },
+
+    "SETTINGS" : {
+
+        "SECTION_HEADER_SETTINGS" : "Configuració"
+
+    },
+
+    "SETTINGS_CONNECTION_HISTORY" : {
+
+        "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD",
+        "ACTION_SEARCH"   : "@:APP.ACTION_SEARCH",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FILENAME_HISTORY_CSV" : "history.csv",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_CONNECTION_HISTORY" : "Aquí es llisten els registres d'historial de connexions anteriors i es poden ordenar fent clic a les capçaleres de columna. Per cercar registres específics, introduïu una cadena de filtre i feu clic a \"Cerca\". Només s'enumeraran els registres que coincideixin amb la cadena de filtre proporcionada.",
+
+        "INFO_CONNECTION_DURATION_UNKNOWN" : "--",
+        "INFO_NO_HISTORY"                  : "No hi ha registres coincidents",
+
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nom de la connexió",
+        "TABLE_HEADER_SESSION_DURATION"        : "Durada",
+        "TABLE_HEADER_SESSION_REMOTEHOST"      : "Amfitrió remot",
+        "TABLE_HEADER_SESSION_STARTDATE"       : "Hora d'inici",
+        "TABLE_HEADER_SESSION_USERNAME"        : "Nom d'usuari",
+
+        "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION"
+
+    },
+
+    "SETTINGS_CONNECTIONS" : {
+
+        "ACTION_ACKNOWLEDGE"          : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_CONNECTION"       : "Nova connexió",
+        "ACTION_NEW_CONNECTION_GROUP" : "Grup nou",
+        "ACTION_NEW_SHARING_PROFILE"  : "Nou perfil per compartir",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_CONNECTIONS"   : "Feu clic o toqueu una connexió següent per gestionar aquesta connexió. Segons el vostre nivell d'accés, es poden afegir i suprimir connexions i es poden canviar les seves propietats (protocol, nom d'amfitrió, port, etc.).",
+        
+        "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
+
+        "SECTION_HEADER_CONNECTIONS"     : "Connexions"
+
+    },
+
+    "SETTINGS_PREFERENCES" : {
+
+        "ACTION_ACKNOWLEDGE"        : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"             : "@:APP.ACTION_CANCEL",
+        "ACTION_UPDATE_PASSWORD"    : "@:APP.ACTION_UPDATE_PASSWORD",
+
+        "DIALOG_HEADER_ERROR"    : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_PASSWORD_BLANK"    : "@:APP.ERROR_PASSWORD_BLANK",
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+
+        "FIELD_HEADER_LANGUAGE"           : "Llenguatge de visualització:",
+        "FIELD_HEADER_PASSWORD"           : "Contrasenya:",
+        "FIELD_HEADER_PASSWORD_OLD"       : "Contrasenya actual:",
+        "FIELD_HEADER_PASSWORD_NEW"       : "Nova contrasenya:",
+        "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Confirmar nova contrasenya:",
+        "FIELD_HEADER_TIMEZONE"           : "Fus horari:",
+        "FIELD_HEADER_USERNAME"           : "Nom d'usuari:",
+        
+        "HELP_DEFAULT_INPUT_METHOD" : "El mètode d'introducció predeterminat determina com Guacamole rep els esdeveniments del teclat. Canviar aquesta configuració pot ser necessària quan s'utilitza un dispositiu mòbil o quan s'escriu a través d'un IME. Aquesta configuració es pot substituir de manera per connexió dins del menú Guacamole.",
+        "HELP_DEFAULT_MOUSE_MODE"   : "El mode d'emulació predeterminat del ratolí determina com es comportarà el ratolí remot en les noves connexions pel que fa als tocs. Aquesta configuració es pot substituir de manera per connexió al menú Guacamole.",
+        "HELP_INPUT_METHOD_NONE"    : "@:CLIENT.HELP_INPUT_METHOD_NONE",
+        "HELP_INPUT_METHOD_OSK"     : "@:CLIENT.HELP_INPUT_METHOD_OSK",
+        "HELP_INPUT_METHOD_TEXT"    : "@:CLIENT.HELP_INPUT_METHOD_TEXT",
+        "HELP_LOCALE"               : "Les opcions que apareixen a continuació estan relacionades amb la configuració regional de l'usuari i tindran un impacte sobre la visualització de diverses parts de la interfície.",
+        "HELP_MOUSE_MODE_ABSOLUTE"  : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE",
+        "HELP_MOUSE_MODE_RELATIVE"  : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE",
+        "HELP_UPDATE_PASSWORD"      : "Si voleu canviar la vostra contrasenya, introduïu la vostra contrasenya actual i la contrasenya desitjada a continuació, i feu clic a \"Actualitzar la contrasenya\". El canvi entrarà en vigor immediatament.",
+
+        "INFO_PASSWORD_CHANGED" : "S'ha canviat la contrasenya.",
+
+        "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE",
+        "NAME_INPUT_METHOD_OSK"  : "@:CLIENT.NAME_INPUT_METHOD_OSK",
+        "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT",
+
+        "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Mètode d'introducció per defecte",
+        "SECTION_HEADER_DEFAULT_MOUSE_MODE"   : "Mode d'emulació del ratolí predeterminat",
+        "SECTION_HEADER_UPDATE_PASSWORD"      : "Canvia la contrasenya"
+
+    },
+
+    "SETTINGS_USERS" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER"      : "Nou usuari",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USERS" : "Feu clic o toqueu un usuari per gestionar aquest usuari. En funció del vostre nivell d'accés, es poden afegir i eliminar els usuaris i es poden canviar les seves contrasenyes.",
+
+        "SECTION_HEADER_USERS"       : "Usuaris",
+
+        "TABLE_HEADER_FULL_NAME"   : "Nom complet",
+        "TABLE_HEADER_LAST_ACTIVE" : "Darrer actiu",
+        "TABLE_HEADER_ORGANIZATION" : "Organització",
+        "TABLE_HEADER_USERNAME"    : "Nom d'usuari"
+
+    },
+
+    "SETTINGS_USER_GROUPS" : {
+
+        "ACTION_ACKNOWLEDGE"    : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER_GROUP" : "Grup nou",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USER_GROUPS" : "Feu clic o toqueu un grup següent per gestionar aquest grup. En funció del vostre nivell d'accés, es poden afegir i suprimir grups i es poden canviar els usuaris i grups.",
+
+        "SECTION_HEADER_USER_GROUPS" : "Grups",
+
+        "TABLE_HEADER_USER_GROUP_NAME" : "Nom del grup"
+
+    },
+
+    "SETTINGS_SESSIONS" : {
+        
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_DELETE"      : "Finalitzar Sessions",
+        
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Finalitzar Sessions",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+        
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+        
+        "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_SESSIONS" : "Aquesta pàgina s'omple de connexions actualment actives. Les connexions enumerades i la possibilitat de destruir aquestes connexions depèn del nivell d'accés. Si voleu destruir una o més sessions, marqueu la casella que hi ha al costat de les sessions i feu clic a \"Destruir Sessions\". Destruir una sessió desconnectarà immediatament l'usuari de la connexió associada.",
+        
+        "INFO_NO_SESSIONS" : "No hi ha sessions actives",
+
+        "SECTION_HEADER_SESSIONS" : "Sessions actives",
+        
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nom de la connexió",
+        "TABLE_HEADER_SESSION_REMOTEHOST"      : "Amfitrió remot",
+        "TABLE_HEADER_SESSION_STARTDATE"       : "Actiu des de",
+        "TABLE_HEADER_SESSION_USERNAME"        : "Nom d'usuari",
+        
+        "TEXT_CONFIRM_DELETE" : "Esteu segur que voleu matar totes les sessions seleccionades? Els usuaris que utilitzin aquestes sessions seran desconnectats immediatament."
+
+    },
+
+    "USER_ATTRIBUTES" : {
+
+        "FIELD_HEADER_GUAC_EMAIL_ADDRESS"       : "Correu electrònic:",
+        "FIELD_HEADER_GUAC_FULL_NAME"           : "Nom complet:",
+        "FIELD_HEADER_GUAC_ORGANIZATION"        : "Organització:",
+        "FIELD_HEADER_GUAC_ORGANIZATIONAL_ROLE" : "Role:"
+
+    },
+
+    "USER_MENU" : {
+
+        "ACTION_LOGOUT"             : "@:APP.ACTION_LOGOUT",
+        "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS",
+        "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES",
+        "ACTION_MANAGE_SESSIONS"    : "@:APP.ACTION_MANAGE_SESSIONS",
+        "ACTION_MANAGE_SETTINGS"    : "@:APP.ACTION_MANAGE_SETTINGS",
+        "ACTION_MANAGE_USERS"       : "@:APP.ACTION_MANAGE_USERS",
+        "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS",
+        "ACTION_NAVIGATE_HOME"      : "@:APP.ACTION_NAVIGATE_HOME",
+        "ACTION_VIEW_HISTORY"       : "@:APP.ACTION_VIEW_HISTORY"
+
+    }
+
+}
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json
index 47e4b71..caf1b0a 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -54,7 +54,9 @@
     "CLIENT" : {
 
         "ACTION_ACKNOWLEDGE"               : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"                    : "@:APP.ACTION_CANCEL",
         "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear",
+        "ACTION_CONTINUE"                  : "@:APP.ACTION_CONTINUE",
         "ACTION_DISCONNECT"                : "Disconnect",
         "ACTION_LOGOUT"                    : "@:APP.ACTION_LOGOUT",
         "ACTION_NAVIGATE_BACK"             : "@:APP.ACTION_NAVIGATE_BACK",
diff --git a/pom.xml b/pom.xml
index 84dbac6..f0c24de 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-client</artifactId>
     <packaging>pom</packaging>
-    <version>1.2.0</version>
+    <version>1.3.0</version>
     <name>guacamole-client</name>
     <url>http://guacamole.apache.org/</url>