GUACAMOLE-1218: Merge "guacamole-auth-json" extension into Apache Guacamole.

diff --git a/extensions/guacamole-auth-cas/pom.xml b/extensions/guacamole-auth-cas/pom.xml
index f4b30e4..2ffe818 100644
--- a/extensions/guacamole-auth-cas/pom.xml
+++ b/extensions/guacamole-auth-cas/pom.xml
@@ -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 b1a9b8d..2ebf5d4 100644
--- a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
@@ -13,9 +13,10 @@
         "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
+        "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ]
-
 }
diff --git a/extensions/guacamole-auth-cas/src/main/resources/translations/fr.json b/extensions/guacamole-auth-cas/src/main/resources/translations/fr.json
new file mode 100644
index 0000000..5177772
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/resources/translations/fr.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_CAS" : {
+        "NAME" : "CAS SSO Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_TICKET"        : "",
+        "INFO_CAS_REDIRECT_PENDING" : "Veuillez patienter, redirection vers l'authentification CAS..."
+    }
+
+}
diff --git a/extensions/guacamole-auth-cas/src/main/resources/translations/zh.json b/extensions/guacamole-auth-cas/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..81491de
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/resources/translations/zh.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_CAS" : {
+        "NAME" : "CAS SSO后端"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_TICKET"        : "",
+        "INFO_CAS_REDIRECT_PENDING"  : "请稍候,正在重定向到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/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
index 71a5634..b2c9208 100644
--- a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
@@ -13,9 +13,11 @@
         "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
+        "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/fr.json b/extensions/guacamole-auth-duo/src/main/resources/translations/fr.json
new file mode 100644
index 0000000..027bf83
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/translations/fr.json
@@ -0,0 +1,13 @@
+{
+
+    "DATA_SOURCE_DUO" : {
+        "NAME" : "Duo TFA Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "",
+        "INFO_DUO_VALIDATION_CODE_INCORRECT"    : "Code de validation Duo incorrect.",
+        "INFO_DUO_AUTH_REQUIRED"                : "Veuillez vous authentifier avec Duo pour continuer."
+    }
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/zh.json b/extensions/guacamole-auth-duo/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..8bee1e2
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/translations/zh.json
@@ -0,0 +1,13 @@
+{
+
+    "DATA_SOURCE_DUO" : {
+        "NAME" : "Duo TFA后端"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "",
+        "INFO_DUO_VALIDATION_CODE_INCORRECT"    : "Duo验证码不正确。",
+        "INFO_DUO_AUTH_REQUIRED"                : "请先使用Duo进行身份验证。"
+    }
+
+}
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/resources/translations/fr.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/fr.json
index 8521f58..a85fad4 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/fr.json
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/fr.json
@@ -3,24 +3,110 @@
     "LOGIN" : {
 
         "ERROR_PASSWORD_BLANK"    : "@:APP.ERROR_PASSWORD_BLANK",
+        "ERROR_PASSWORD_SAME"     : "le nouveau mot de passe doit être différent du mot de passe expiré.",
         "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+        "ERROR_NOT_VALID"         : "Ce compte utilisateur n'est pas valide pour le moment.",
+        "ERROR_NOT_ACCESSIBLE"    : "L'accès à ce compte n'est pas autorisé pour le moment. Veuillez réessayer plus tard.",
 
-        "FIELD_HEADER_NEW_PASSWORD"         : "Mot de passe",
-        "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Répéter mot de passe"
+        "INFO_PASSWORD_EXPIRED" : "Votre mot de passe a expiré et doit être changé. Veuillez entrer un nouveau mot de passe pour continuer.",
+
+        "FIELD_HEADER_NEW_PASSWORD"         : "Nouveau mot de passe",
+        "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Confirmez le nouveau mot de passe"
+
+    },
+
+    "CONNECTION_ATTRIBUTES" : {
+
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "Nombre maximum de connexions:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Nombre maximum de connexions par utilisateur:",
+
+        "FIELD_HEADER_FAILOVER_ONLY"            : "Utilisé seulement en cas de bascule:",
+        "FIELD_HEADER_WEIGHT"                   : "Poids de la connexion:",
+
+        "FIELD_HEADER_GUACD_HOSTNAME"   : "Nom d'hôte:",
+        "FIELD_HEADER_GUACD_ENCRYPTION" : "Chiffrement:",
+        "FIELD_HEADER_GUACD_PORT"       : "Port:",
+
+        "FIELD_OPTION_GUACD_ENCRYPTION_EMPTY" : "",
+        "FIELD_OPTION_GUACD_ENCRYPTION_NONE"  : "Aucun (non-chiffré)",
+        "FIELD_OPTION_GUACD_ENCRYPTION_SSL"   : "SSL / TLS",
+
+        "SECTION_HEADER_CONCURRENCY"    : "Limites de concurrence",
+        "SECTION_HEADER_LOAD_BALANCING" : "Equilibrage de charge",
+        "SECTION_HEADER_GUACD"          : "Paramètres du proxy Guacamole (guacd)"
+
+    },
+
+    "CONNECTION_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_ENABLE_SESSION_AFFINITY"  : "Activer l'affinité de session:",
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "Nombre maximum de connexions:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Nombre maximum de connexions par utilisateur:",
+
+        "SECTION_HEADER_CONCURRENCY" : "Limites de concurrence (Groupes de répartition)"
+
+    },
+
+    "DATA_SOURCE_MYSQL" : {
+        "NAME" : "MySQL"
+    },
+
+    "DATA_SOURCE_MYSQL_SHARED" : {
+        "NAME" : "Connexions partagées (MySQL)"
+    },
+
+    "DATA_SOURCE_POSTGRESQL" : {
+        "NAME" : "PostgreSQL"
+    },
+
+    "DATA_SOURCE_POSTGRESQL_SHARED" : {
+        "NAME" : "Connexions partagées (PostgreSQL)"
+    },
+
+    "DATA_SOURCE_SQLSERVER" : {
+        "NAME" : "SQL Server"
+    },
+
+    "DATA_SOURCE_SQLSERVER_SHARED" : {
+        "NAME" : "Connexions partagées (SQL Server)"
+    },
+
+    "HOME" : {
+        "INFO_SHARED_BY" : "Partagé par {USERNAME}"
+    },
+
+    "PASSWORD_POLICY" : {
+
+        "ERROR_CONTAINS_USERNAME"      : "Les mots de passe ne doivent pas contenir le nom d'utilisateur.",
+        "ERROR_REQUIRES_DIGIT"         : "Les mots de passe doivent contenir au moins un chiffre.",
+        "ERROR_REQUIRES_MULTIPLE_CASE" : "Les mots de passe doivent contenir des caractères minuscules et majuscules.",
+        "ERROR_REQUIRES_NON_ALNUM"     : "Les mots de passe doivent contenir au moins un symbole.",
+        "ERROR_REUSED"                 : "Ce mot de passe a déjà été utilisé. Veuillez ne pas réutiliser {HISTORY_SIZE} {HISTORY_SIZE, plural, one{le dernier mot de passe} other{un des derniers mots de passe}}.",
+        "ERROR_TOO_SHORT"              : "Les mots de passe doivent être composé d'au moins {LENGTH} {LENGTH, plural, one{caractère} other{caractères}}.",
+        "ERROR_TOO_YOUNG"              : "Le mot de passe pour ce compte a déjà été réinitialisé. Veuillez patienter au moins {WAIT} {WAIT, plural, one{jour} other{jours}} avant de changer à nouveau le mot de passe."
 
     },
 
     "USER_ATTRIBUTES" : {
 
-        "FIELD_HEADER_DISABLED"            : "Identifiant désactivé:",
+        "FIELD_HEADER_DISABLED"            : "Connexion désactivée:",
         "FIELD_HEADER_EXPIRED"             : "Mot de passe expiré:",
-        "FIELD_HEADER_ACCESS_WINDOW_END"   : "Interdire l'accès après:",
+        "FIELD_HEADER_ACCESS_WINDOW_END"   : "Ne pas autoriser l'accès après:",
         "FIELD_HEADER_ACCESS_WINDOW_START" : "Autoriser l'accès après:",
-        "FIELD_HEADER_TIMEZONE"            : "Fuseau horaire de l'utilisateur:",
+        "FIELD_HEADER_TIMEZONE"            : "Fuseau horaire utilisateur:",
         "FIELD_HEADER_VALID_FROM"          : "Activer le compte après:",
         "FIELD_HEADER_VALID_UNTIL"         : "Désactiver le compte après:",
 
-        "SECTION_HEADER_RESTRICTIONS" : "Restrictions de comptes"
+        "SECTION_HEADER_RESTRICTIONS" : "Restrictions de compte",
+        "SECTION_HEADER_PROFILE"      : "Profil"
+
+    },
+
+    "USER_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_DISABLED" : "Désactivé:",
+
+        "SECTION_HEADER_RESTRICTIONS" : "Restrictions de groupe"
 
     }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/zh.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..44b6bf2
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/zh.json
@@ -0,0 +1,113 @@
+{
+
+    "LOGIN" : {
+
+        "ERROR_PASSWORD_BLANK"    : "@:APP.ERROR_PASSWORD_BLANK",
+        "ERROR_PASSWORD_SAME"     : "新密码必须与过期密码不同。",
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+        "ERROR_NOT_VALID"         : "此用户帐户当前无效。",
+        "ERROR_NOT_ACCESSIBLE"    : "当前不允许访问该帐户。请稍后再试。",
+
+        "INFO_PASSWORD_EXPIRED" : "您的密码已过期,必须重新设置。请输入新密码以继续。",
+
+        "FIELD_HEADER_NEW_PASSWORD"         : "新密码",
+        "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "确认新密码"
+
+    },
+
+    "CONNECTION_ATTRIBUTES" : {
+
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "最大连接数:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "每个用户的最大连接数:",
+
+        "FIELD_HEADER_FAILOVER_ONLY"            : "仅用于故障转移:",
+        "FIELD_HEADER_WEIGHT"                   : "连接权重:",
+
+        "FIELD_HEADER_GUACD_HOSTNAME"   : "主机名:",
+        "FIELD_HEADER_GUACD_ENCRYPTION" : "加密:",
+        "FIELD_HEADER_GUACD_PORT"       : "端口:",
+
+        "FIELD_OPTION_GUACD_ENCRYPTION_EMPTY" : "",
+        "FIELD_OPTION_GUACD_ENCRYPTION_NONE"  : "没有 (未加密)",
+        "FIELD_OPTION_GUACD_ENCRYPTION_SSL"   : "SSL / TLS",
+
+        "SECTION_HEADER_CONCURRENCY"    : "并发限制",
+        "SECTION_HEADER_LOAD_BALANCING" : "负载均衡",
+        "SECTION_HEADER_GUACD"          : "Guacamole代理参数(guacd)"
+
+    },
+
+    "CONNECTION_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_ENABLE_SESSION_AFFINITY"  : "启用会话亲和:",
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "最大连接数:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "每个用户的最大连接数:",
+
+        "SECTION_HEADER_CONCURRENCY" : "并发限制(组负载均衡)"
+
+    },
+
+    "DATA_SOURCE_MYSQL" : {
+        "NAME" : "MySQL"
+    },
+
+    "DATA_SOURCE_MYSQL_SHARED" : {
+        "NAME" : "共享连接(MySQL)"
+    },
+
+    "DATA_SOURCE_POSTGRESQL" : {
+        "NAME" : "PostgreSQL"
+    },
+
+    "DATA_SOURCE_POSTGRESQL_SHARED" : {
+        "NAME" : "共享连接(PostgreSQL)"
+    },
+
+    "DATA_SOURCE_SQLSERVER" : {
+        "NAME" : "SQL Server"
+    },
+
+    "DATA_SOURCE_SQLSERVER_SHARED" : {
+        "NAME" : "共享连接(SQL Server)"
+    },
+
+    "HOME" : {
+        "INFO_SHARED_BY" : "由{USERNAME}共享"
+    },
+
+    "PASSWORD_POLICY" : {
+
+        "ERROR_CONTAINS_USERNAME"      : "密码可能不包含用户名。",
+        "ERROR_REQUIRES_DIGIT"         : "密码必须至少包含一位数字。",
+        "ERROR_REQUIRES_MULTIPLE_CASE" : "密码必须同时包含大写和小写字符。",
+        "ERROR_REQUIRES_NON_ALNUM"     : "密码必须包含至少一个符号。",
+        "ERROR_REUSED"                 : "此密码已被使用。请勿重复使用以前的密码。",
+        "ERROR_TOO_SHORT"              : "密码必须至少{LENGTH}位长度。",
+        "ERROR_TOO_YOUNG"              : "此帐户的密码已被重设。请至少再等待 {WAIT}天,然后再次更改密码。"
+
+    },
+
+    "USER_ATTRIBUTES" : {
+
+        "FIELD_HEADER_DISABLED"            : "已禁用登录:",
+        "FIELD_HEADER_EXPIRED"             : "密码已过期:",
+        "FIELD_HEADER_ACCESS_WINDOW_END"   : "之后不允许访问:",
+        "FIELD_HEADER_ACCESS_WINDOW_START" : "之后允许访问:",
+        "FIELD_HEADER_TIMEZONE"            : "用户时区:",
+        "FIELD_HEADER_VALID_FROM"          : "之后启用帐户:",
+        "FIELD_HEADER_VALID_UNTIL"         : "之后禁用帐户:",
+
+        "SECTION_HEADER_RESTRICTIONS" : "帐户限制",
+        "SECTION_HEADER_PROFILE"      : "个人资料"
+
+    },
+
+    "USER_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_DISABLED" : "禁用:",
+
+        "SECTION_HEADER_RESTRICTIONS" : "组限制"
+
+    }
+
+}
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 201b680..f3ea908 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
@@ -26,7 +26,8 @@
         "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ]
 
 }
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 8bc54f2..312ad88 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
@@ -26,7 +26,8 @@
         "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ]
 
 }
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 bba5d22..3c7c679 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
@@ -26,7 +26,8 @@
         "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ]
 
 }
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 58abcdf..c473432 100644
--- a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
@@ -11,7 +11,8 @@
 
     "translations" : [
         "translations/de.json",
-        "translations/en.json"
+        "translations/en.json",
+        "translations/zh.json"
     ]
 
 }
diff --git a/extensions/guacamole-auth-ldap/src/main/resources/translations/zh.json b/extensions/guacamole-auth-ldap/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..e842ecb
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/main/resources/translations/zh.json
@@ -0,0 +1,7 @@
+{
+
+    "DATA_SOURCE_LDAP" : {
+        "NAME" : "轻型目录访问协议"
+    }
+
+}
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 d888d96..9090d22 100644
--- a/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
@@ -13,9 +13,11 @@
         "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
+        "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-openid/src/main/resources/translations/fr.json b/extensions/guacamole-auth-openid/src/main/resources/translations/fr.json
new file mode 100644
index 0000000..a8d45e6
--- /dev/null
+++ b/extensions/guacamole-auth-openid/src/main/resources/translations/fr.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_OPENID" : {
+        "NAME" : "OpenID SSO Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_ID_TOKEN" : "",
+        "INFO_OID_REDIRECT_PENDING" : "Veuillez patienter, redirection vers le fournisseur d'identité..."
+    }
+
+}
diff --git a/extensions/guacamole-auth-openid/src/main/resources/translations/zh.json b/extensions/guacamole-auth-openid/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..a903eb4
--- /dev/null
+++ b/extensions/guacamole-auth-openid/src/main/resources/translations/zh.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_OPENID" : {
+        "NAME" : "OpenID SSO后端"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_ID_TOKEN" : "",
+        "INFO_REDIRECT_PENDING" : "请稍候,正在重定向到身份提供者..."
+    }
+
+}
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 21d7f36..0e1a40d 100644
--- a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
@@ -24,9 +24,11 @@
         "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
+        "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ],
 
     "resources" : {
diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/translations/fr.json b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/fr.json
new file mode 100644
index 0000000..7bfe51b
--- /dev/null
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/fr.json
@@ -0,0 +1,18 @@
+{
+
+    "DATA_SOURCE_QUICKCONNECT" : {
+        "NAME" : "QuickConnect"
+    },
+
+    "QUICKCONNECT" : {
+        "ACTION_CONNECT"        : "Connecter",
+        
+        "ERROR_INVALID_URI"      : "L'URI spécifiée est invalide",
+        "ERROR_NO_HOST"          : "Pas d'hôte spécifié",
+        "ERROR_NO_PROTOCOL"      : "Pas de protocole spécifié",
+        "ERROR_NOT_ABSOLUTE_URI" : "L'URI n'est pas absolue",
+        
+        "FIELD_PLACEHOLDER_URI" : "Entrez l'URI de connexion"
+    }
+
+}
diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/translations/zh.json b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..c214de0
--- /dev/null
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/zh.json
@@ -0,0 +1,18 @@
+{
+
+    "DATA_SOURCE_QUICKCONNECT" : {
+        "NAME" : "快速连接"
+    },
+
+    "QUICKCONNECT" : {
+        "ACTION_CONNECT"        : "连接",
+        
+        "ERROR_INVALID_URI"      : "指定的URI无效",
+        "ERROR_NO_HOST"          : "未指定主机",
+        "ERROR_NO_PROTOCOL"      : "未指定协议",
+        "ERROR_NOT_ABSOLUTE_URI" : "不是绝对URI地址",
+        
+        "FIELD_PLACEHOLDER_URI" : "输入连接URI"
+    }
+
+}
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 c43f32e..0ea8538 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
@@ -14,7 +14,8 @@
         "translations/de.json",
         "translations/en.json",
         "translations/ja.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-radius/src/main/resources/translations/zh.json b/extensions/guacamole-auth-radius/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..6f697b8
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/resources/translations/zh.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_RADIUS" : {
+        "NAME" : "RADIUS后端"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_RADIUS_STATE"              : "",
+        "FIELD_HEADER_RADIUSCHALLENGE"                : ""
+    }
+
+}
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 9db1355..56a607a 100644
--- a/extensions/guacamole-auth-saml/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-saml/src/main/resources/guac-manifest.json
@@ -12,6 +12,7 @@
     "translations" : [
         "translations/ca.json",
         "translations/en.json",
+        "translations/fr.json",
         "translations/pt.json"
     ]
 
diff --git a/extensions/guacamole-auth-saml/src/main/resources/translations/fr.json b/extensions/guacamole-auth-saml/src/main/resources/translations/fr.json
new file mode 100644
index 0000000..43108d2
--- /dev/null
+++ b/extensions/guacamole-auth-saml/src/main/resources/translations/fr.json
@@ -0,0 +1,12 @@
+{
+
+    "DATA_SOURCE_SAML" : {
+        "NAME" : "SAML Authentication Extension"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_SAML"     : "",
+        "INFO_SAML_REDIRECT_PENDING" : "Veuillez patienter, redirection vers le fournisseur d'identité..."
+    }
+
+}
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 7e74b2d..23bdd66 100644
--- a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
@@ -13,9 +13,11 @@
         "translations/ca.json",
         "translations/de.json",
         "translations/en.json",
+        "translations/fr.json",
         "translations/ja.json",
         "translations/pt.json",
-        "translations/ru.json"
+        "translations/ru.json",
+        "translations/zh.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/fr.json b/extensions/guacamole-auth-totp/src/main/resources/translations/fr.json
new file mode 100644
index 0000000..db785bd
--- /dev/null
+++ b/extensions/guacamole-auth-totp/src/main/resources/translations/fr.json
@@ -0,0 +1,34 @@
+{
+
+    "DATA_SOURCE_TOTP" : {
+        "NAME" : "TOTP TFA Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_TOTP" : ""
+    },
+
+    "TOTP" : {
+
+        "ACTION_HIDE_DETAILS" : "Masquer",
+        "ACTION_SHOW_DETAILS" : "Montrer",
+
+        "FIELD_HEADER_ALGORITHM"  : "Algorithme:",
+        "FIELD_HEADER_DIGITS"     : "Chiffres:",
+        "FIELD_HEADER_INTERVAL"   : "Intervalle:",
+        "FIELD_HEADER_SECRET_KEY" : "Clé secrète:",
+
+        "FIELD_PLACEHOLDER_CODE" : "Code d'authentification",
+
+        "INFO_CODE_REQUIRED"       : "Veuillez entrer le code d'authentification pour vérifier votre identité.",
+        "INFO_ENROLL_REQUIRED"     : "L'authentification multi-facteurs a été activée pour votre compte.",
+        "INFO_VERIFICATION_FAILED" : "La vérification a échoué. Veuillez réessayer.",
+
+        "HELP_ENROLL_BARCODE" : "Pour terminer votre processus d'inscription, scannez le code-barre ci-dessous avec l'application deux-facteurs sur votre téléphone ou votre appareil",
+        "HELP_ENROLL_VERIFY"  : "Après avoir scanné le code-barre, saisissez les {DIGITS} chiffres du code d'authentification affichés pour terminer votre inscription.",
+
+        "SECTION_HEADER_DETAILS" : "Détails:"
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/zh.json b/extensions/guacamole-auth-totp/src/main/resources/translations/zh.json
new file mode 100644
index 0000000..9d27667
--- /dev/null
+++ b/extensions/guacamole-auth-totp/src/main/resources/translations/zh.json
@@ -0,0 +1,34 @@
+{
+
+    "DATA_SOURCE_TOTP" : {
+        "NAME" : "TOTP TFA后端"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_TOTP" : ""
+    },
+
+    "TOTP" : {
+
+        "ACTION_HIDE_DETAILS" : "隐藏",
+        "ACTION_SHOW_DETAILS" : "显示",
+
+        "FIELD_HEADER_ALGORITHM"  : "算法:",
+        "FIELD_HEADER_DIGITS"     : "位数:",
+        "FIELD_HEADER_INTERVAL"   : "间隔:",
+        "FIELD_HEADER_SECRET_KEY" : "密钥:",
+
+        "FIELD_PLACEHOLDER_CODE" : "授权码",
+
+        "INFO_CODE_REQUIRED"       : "请输入您的授权码以验证您的身份。",
+        "INFO_ENROLL_REQUIRED"     : "您的帐户已启用多因素身份验证。",
+        "INFO_VERIFICATION_FAILED" : "验证失败, 请重试。",
+
+        "HELP_ENROLL_BARCODE" : "要完成注册过程,请使用手机或设备上的two-factor验证程序扫描下面的条形码。",
+        "HELP_ENROLL_VERIFY"  : "扫描条形码后,输入显示的{DIGITS}-数字授权码以验证注册是否成功。",
+
+        "SECTION_HEADER_DETAILS" : "详情:"
+
+    }
+
+}
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/pom.xml b/guacamole-common/pom.xml
index b8e09d4..f9fb82a 100644
--- a/guacamole-common/pom.xml
+++ b/guacamole-common/pom.xml
@@ -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-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/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/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
index b40a949..12b9f57 100644
--- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js
+++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
@@ -386,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() {
@@ -612,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);
         }
         
@@ -640,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);
                 }
 
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/fr.json b/guacamole/src/main/webapp/translations/fr.json
index 014bc0e..c87a09c 100644
--- a/guacamole/src/main/webapp/translations/fr.json
+++ b/guacamole/src/main/webapp/translations/fr.json
@@ -30,7 +30,6 @@
         "DIALOG_HEADER_ERROR" : "Erreur",
 
         "ERROR_PAGE_UNAVAILABLE"  : "Une erreur est apparue et cette action ne pourra pas être achevé. Si le problème persiste, merci de contacter votre administrateur ou regarder les journaux système.",
-
         "ERROR_PASSWORD_BLANK"    : "Votre mot de passe ne peut pas être vide.",
         "ERROR_PASSWORD_MISMATCH" : "Le mot de passe ne correspond pas.",
 
@@ -42,8 +41,8 @@
         "FORMAT_DATE_TIME_PRECISE" : "dd-MM-yyyy HH:mm:ss",
 
         "INFO_ACTIVE_USER_COUNT" : "Actuellement utilisé par {USERS} {USERS, plural, one{utilisateur} other{utilisateurs}}.",
-        "TEXT_ANONYMOUS_USER"   : "Anonyme",
 
+        "TEXT_ANONYMOUS_USER"   : "Anonyme",
         "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{seconde} other{secondes}}} minute{{VALUE, plural, one{minute} other{minutes}}} hour{{VALUE, plural, one{heure} other{heures}}} day{{VALUE, plural, one{jour} other{jours}}} other{}}",
         "TEXT_UNTRANSLATED" : "{MESSAGE}"
 
@@ -132,7 +131,7 @@
         "SECTION_HEADER_CLIPBOARD"      : "Presse-papiers",
         "SECTION_HEADER_DEVICES"        : "Appareils",
         "SECTION_HEADER_DISPLAY"        : "Affichage",
-        "SECTION_HEADER_FILE_TRANSFERS" : "Transfers de fichiers",
+        "SECTION_HEADER_FILE_TRANSFERS" : "Transferts de fichiers",
         "SECTION_HEADER_INPUT_METHOD"   : "Méthode de saisie",
         "SECTION_HEADER_MOUSE_MODE"     : "Mode émulation souris",
 
@@ -194,7 +193,7 @@
 
     },
 
-    "LIST":{
+    "LIST": {
 
          "TEXT_ANONYMOUS_USER" : "Anonyme"
 
@@ -308,13 +307,13 @@
 
         "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
 
-        "FIELD_HEADER_ADMINISTER_SYSTEM"             : "Administrateur du système:",
+        "FIELD_HEADER_ADMINISTER_SYSTEM"             : "Administration du système:",
         "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "Modifier son propre mot de passe:",
-        "FIELD_HEADER_CREATE_NEW_USERS"              : "Créer nouveaux utilisateurs:",
-        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "Créer nouveaux groupes d'utilisateurs:",
-        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "Créer nouvelles connexions:",
-        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "Créer nouveaux groupes de connexion:",
-        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "Créer nouveaux profils de partage:",
+        "FIELD_HEADER_CREATE_NEW_USERS"              : "Créer de nouveaux utilisateurs:",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "Créer de nouveaux groupes d'utilisateurs:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "Créer de nouvelles connexions:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "Créer de nouveaux groupes de connexion:",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "Créer de nouveaux profils de partage:",
         "FIELD_HEADER_PASSWORD"                      : "@:APP.FIELD_HEADER_PASSWORD",
         "FIELD_HEADER_PASSWORD_AGAIN"                : "@:APP.FIELD_HEADER_PASSWORD_AGAIN",
         "FIELD_HEADER_USERNAME"                      : "Identifiant:",
@@ -374,7 +373,7 @@
         "SECTION_HEADER_MEMBER_USERS"        : "Utilisateurs Membre",
         "SECTION_HEADER_MEMBER_USER_GROUPS"  : "Groupes Membre",
         "SECTION_HEADER_PERMISSIONS"         : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS",
-        "SECTION_HEADER_USER_GROUPS"         : "Groupe Parent",
+        "SECTION_HEADER_USER_GROUPS"         : "Groupes Parent",
 
         "TEXT_CONFIRM_DELETE" : "Les groupes ne peuvent pas être restaurés après leur suppression. Êtes-vous certains de vouloir supprimer ce groupe?"
 
@@ -455,27 +454,29 @@
         "FIELD_HEADER_CREATE_DRIVE_PATH" : "Créer automatiquement le chemin du lecteur:",
         "FIELD_HEADER_CREATE_RECORDING_PATH" : "Créer automatiquement un chemin d'enregistrement:",
         "FIELD_HEADER_DISABLE_AUDIO"   : "Désactiver son:",
-        "FIELD_HEADER_DISABLE_AUTH"    : "Désactiver authentification:",
+        "FIELD_HEADER_DISABLE_AUTH"    : "Désactiver l'authentification:",
         "FIELD_HEADER_DISABLE_COPY"    : "Désactiver la copie depuis l'ordinateur distant:",
+        "FIELD_HEADER_DISABLE_DOWNLOAD" : "Désactiver le téléchargement de fichier:",
         "FIELD_HEADER_DISABLE_PASTE"   : "Désactiver coller à partir du client:",
-        "FIELD_HEADER_DOMAIN"          : "Nom du domaine:",
+        "FIELD_HEADER_DISABLE_UPLOAD"   : "Désactiver l'envoi de fichier:",
+        "FIELD_HEADER_DOMAIN"          : "Nom de domaine:",
         "FIELD_HEADER_DPI"             : "Résolution (ppp):",
-        "FIELD_HEADER_DRIVE_NAME"      : "Nom du Lecteur:",
+        "FIELD_HEADER_DRIVE_NAME"      : "Nom du lecteur:",
         "FIELD_HEADER_DRIVE_PATH"      : "Chemin du lecteur:",
-        "FIELD_HEADER_ENABLE_AUDIO_INPUT"         : "Activer Entrée Audio (microphone):",
+        "FIELD_HEADER_ENABLE_AUDIO_INPUT"         : "Activer l'entrée audio (microphone):",
         "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Activer la composition du bureau (Aero):",
         "FIELD_HEADER_ENABLE_DRIVE"    : "Activer lecteur réseau:",
-        "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Enable font smoothing (ClearType):",
+        "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Activer le lissage des polices (ClearType):",
         "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Activer pleine fenêtre de glisser:",
         "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Activer les animations de menu:",
         "FIELD_HEADER_DISABLE_BITMAP_CACHING"     : "Désactiver le cache bitmap:",
-        "FIELD_HEADER_DISABLE_GLYPH_CACHING"      : "Désactiver le cache glyph:",
         "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING"  : "Désactiver le cache hors écran :",
+        "FIELD_HEADER_DISABLE_GLYPH_CACHING"      : "Désactiver le cache glyph:",
         "FIELD_HEADER_ENABLE_PRINTING" : "Activer imprimante:",
         "FIELD_HEADER_ENABLE_SFTP"     : "Activer SFTP:",
         "FIELD_HEADER_ENABLE_THEMING"  : "Activer thématisation:",
         "FIELD_HEADER_ENABLE_WALLPAPER" : "Activer fond d'écran:",
-        "FIELD_HEADER_GATEWAY_DOMAIN"   : "Nom du domaine:",
+        "FIELD_HEADER_GATEWAY_DOMAIN"   : "Nom de domaine:",
         "FIELD_HEADER_GATEWAY_HOSTNAME" : "Nom d'hôte:",
         "FIELD_HEADER_GATEWAY_PASSWORD" : "Mot de passe:",
         "FIELD_HEADER_GATEWAY_PORT"     : "Port:",
@@ -488,7 +489,7 @@
         "FIELD_HEADER_PASSWORD"        : "Mot de passe:",
         "FIELD_HEADER_PORT"            : "Port:",
         "FIELD_HEADER_PRINTER_NAME"    : "Nom de l'imprimante redirigée:",
-        "FIELD_HEADER_PRECONNECTION_BLOB" : "Pré-connexion BLOB (VM ID):",
+        "FIELD_HEADER_PRECONNECTION_BLOB" : "Préconnexion BLOB (VM ID):",
         "FIELD_HEADER_PRECONNECTION_ID"   : "Source RDP ID:",
         "FIELD_HEADER_READ_ONLY"       : "Lecture seule:",
         "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclure la souris:",
@@ -503,19 +504,25 @@
         "FIELD_HEADER_SECURITY"        : "Mode de Sécurité:",
         "FIELD_HEADER_SERVER_LAYOUT"   : "Agencement clavier:",
         "FIELD_HEADER_SFTP_DIRECTORY"   : "Répertoire d'upload par défaut:",
+        "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Désactiver le téléchargement de fichier:",
         "FIELD_HEADER_SFTP_HOST_KEY"    : "Clé publique de l'hôte (Base64):",
         "FIELD_HEADER_SFTP_HOSTNAME"    : "Nom d'hôte:",
-        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Intervale keepalive SFTP:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Intervalle keepalive SFTP:",
         "FIELD_HEADER_SFTP_PASSPHRASE"  : "Phrase secrète:",
         "FIELD_HEADER_SFTP_PASSWORD"    : "Mot de passe:",
         "FIELD_HEADER_SFTP_PORT"        : "Port:",
         "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Clé privée:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Dossier racine de l'explorateur de fichier:",
+        "FIELD_HEADER_SFTP_DISABLE_UPLOAD"        : "Désactiver l'envoi de fichier:",
         "FIELD_HEADER_SFTP_USERNAME"    : "Identifiant:",
         "FIELD_HEADER_STATIC_CHANNELS"  : "Noms des canaux statiques:",
         "FIELD_HEADER_TIMEZONE"         : "Fuseau horaire:",
-        "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Dossier racine de l'explorateur de fichier:",
         "FIELD_HEADER_USERNAME"        : "Identifiant:",
         "FIELD_HEADER_WIDTH"           : "Largeur:",
+      "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adresse de diffusion pour les paquets WoL:",
+      "FIELD_HEADER_WOL_MAC_ADDR"       : "Adresse MAC de l'hôte distant:",
+      "FIELD_HEADER_WOL_SEND_PACKET"    : "Envoi de paquets WoL:",
+      "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'attente du démarage de l'hôte:",
 
         "FIELD_OPTION_COLOR_DEPTH_16"    : "Faibles couleurs (16-bit)",
         "FIELD_OPTION_COLOR_DEPTH_24"    : "Vraies couleurs (24-bit)",
@@ -564,10 +571,11 @@
         "SECTION_HEADER_LOAD_BALANCING"     : "Equilibrage de charge",
         "SECTION_HEADER_NETWORK"            : "Réseau",
         "SECTION_HEADER_PERFORMANCE"        : "Performance",
-        "SECTION_HEADER_PRECONNECTION_PDU"  : "Pré-connexion PDU / Hyper-V",
+        "SECTION_HEADER_PRECONNECTION_PDU"  : "Préconnexion PDU / Hyper-V",
         "SECTION_HEADER_RECORDING"          : "Enregistrement écran",
         "SECTION_HEADER_REMOTEAPP"          : "RemoteApp",
-        "SECTION_HEADER_SFTP"               : "SFTP"
+        "SECTION_HEADER_SFTP"               : "SFTP",
+        "SECTION_HEADER_WOL"                : "Wake-on-LAN (WoL)"
 
     },
 
@@ -599,11 +607,17 @@
         "FIELD_HEADER_RECORDING_NAME" : "Nom de l'enregistrement:",
         "FIELD_HEADER_RECORDING_PATH" : "Chemin de l'enregistrement:",
         "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "Intervalle keepalive Serveur:",
+        "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Désactiver le téléchargement de fichier:",
         "FIELD_HEADER_SFTP_ROOT_DIRECTORY"   : "Dossier racine de l'explorateur de fichier:",
+        "FIELD_HEADER_SFTP_DISABLE_UPLOAD"   : "Désactiver l'envoi de fichier:",
         "FIELD_HEADER_TERMINAL_TYPE"   : "Type du terminal:",
         "FIELD_HEADER_TIMEZONE"        : "Fuseau horaire ($TZ):",
         "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom Typescript :",
         "FIELD_HEADER_TYPESCRIPT_PATH" : "Chemin Typescript :",
+        "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adresse de diffusion pour les paquets WoL:",
+        "FIELD_HEADER_WOL_MAC_ADDR"       : "Adresse MAC de l'hôte distant:",
+        "FIELD_HEADER_WOL_SEND_PACKET"    : "Envoi de paquets WoL:",
+        "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'attente du démarage de l'hôte:",
 
         "FIELD_OPTION_BACKSPACE_EMPTY" : "",
         "FIELD_OPTION_BACKSPACE_8"     : "Retour Arrière (Ctrl-H)",
@@ -649,7 +663,8 @@
         "SECTION_HEADER_RECORDING"      : "Enregistrement Ecran",
         "SECTION_HEADER_SESSION"        : "Session / Environnement",
         "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Enregistrement session Texte)",
-        "SECTION_HEADER_SFTP"           : "SFTP"
+        "SECTION_HEADER_SFTP"           : "SFTP",
+        "SECTION_HEADER_WOL"            : "Wake-on-LAN (WoL)"
 
     },
 
@@ -681,6 +696,10 @@
         "FIELD_HEADER_TERMINAL_TYPE"   : "Type de terminal:",
         "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom Typescript:",
         "FIELD_HEADER_TYPESCRIPT_PATH" : "Chemin Typescript:",
+        "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adresse de diffusion pour les paquets WoL:",
+        "FIELD_HEADER_WOL_MAC_ADDR"       : "Adresse MAC de l'hôte distant:",
+        "FIELD_HEADER_WOL_SEND_PACKET"    : "Envoi de paquets WoL:",
+        "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'attente du démarage de l'hôte:",
 
         "FIELD_OPTION_BACKSPACE_EMPTY" : "",
         "FIELD_OPTION_BACKSPACE_8"     : "Retour Arrière (Ctrl-H)",
@@ -724,7 +743,8 @@
         "SECTION_HEADER_DISPLAY"        : "Affichage",
         "SECTION_HEADER_NETWORK"        : "Réseau",
         "SECTION_HEADER_RECORDING"      : "Enregistrement Ecran",
-        "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Enregistrement session Texte)"
+        "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Enregistrement session Texte)",
+        "SECTION_HEADER_WOL"            : "Wake-on-LAN (WoL)"
 
     },
 
@@ -752,16 +772,22 @@
         "FIELD_HEADER_RECORDING_NAME"   : "Nom de l'enregistrement:",
         "FIELD_HEADER_RECORDING_PATH"   : "Chemin de l'enregistrement:",
         "FIELD_HEADER_SFTP_DIRECTORY"   : "Répertoire d'upload par défaut:",
+        "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Désactiver le téléchargement de fichier:",
         "FIELD_HEADER_SFTP_HOST_KEY"    : "Clé publique de l'hôte (Base64):",
         "FIELD_HEADER_SFTP_HOSTNAME"    : "Nom d'hôte:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Intervale keepalive SFTP:",
         "FIELD_HEADER_SFTP_PASSPHRASE"  : "Phrase secrète:",
         "FIELD_HEADER_SFTP_PASSWORD"    : "Mot de passe:",
         "FIELD_HEADER_SFTP_PORT"        : "Port:",
         "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Clé privée:",
         "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Dossier racine de l'explorateur de fichier:",
-        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Intervale keepalive SFTP:",
+        "FIELD_HEADER_SFTP_DISABLE_UPLOAD"        : "Désactiver l'envoi de fichier:",
         "FIELD_HEADER_SFTP_USERNAME"    : "Identifiant:",
         "FIELD_HEADER_SWAP_RED_BLUE"    : "Inverser composantes rouges et bleues:",
+        "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adresse de diffusion pour les paquets WoL:",
+        "FIELD_HEADER_WOL_MAC_ADDR"       : "Adresse MAC de l'hôte distant:",
+        "FIELD_HEADER_WOL_SEND_PACKET"    : "Envoi de paquets WoL:",
+        "FIELD_HEADER_WOL_WAIT_TIME"      : "Temps d'attente du démarage de l'hôte:",
 
         "FIELD_OPTION_COLOR_DEPTH_8"     : "256 couleurs",
         "FIELD_OPTION_COLOR_DEPTH_16"    : "Faibles couleurs (16-bit)",
@@ -787,8 +813,9 @@
         "SECTION_HEADER_DISPLAY"        : "Affichage",
         "SECTION_HEADER_NETWORK"        : "Réseau",
         "SECTION_HEADER_RECORDING"      : "Enregistrement Ecran",
-        "SECTION_HEADER_REPEATER"       : "Répéteur VNC",
-        "SECTION_HEADER_SFTP"           : "SFTP"
+        "SECTION_HEADER_REPEATER"       : "Répétiteur VNC",
+        "SECTION_HEADER_SFTP"           : "SFTP",
+        "SECTION_HEADER_WOL"            : "Wake-on-LAN (WoL)"
 
     },
 
@@ -798,26 +825,8 @@
 
     },
 
-    "SETTINGS_CONNECTIONS" : {
-
-        "ACTION_ACKNOWLEDGE"          : "@:APP.ACTION_ACKNOWLEDGE",
-        "ACTION_NEW_CONNECTION"       : "Nouvelle Connexion",
-        "ACTION_NEW_CONNECTION_GROUP" : "Nouveau Groupe",
-        "ACTION_NEW_SHARING_PROFILE"  : "Nouveau Profil de Partage",
-
-        "DIALOG_HEADER_ERROR"          : "Erreur",
-
-        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
-
-        "HELP_CONNECTIONS"   : "Cliquer ou appuyer sur une connexion en dessous pour la gérer. Selon vos permissions, les connexions peuvent être ajoutées, supprimées, leur propriétés (protocole, nom d'hôte, port, etc) changées.",
-
-        "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
-
-        "SECTION_HEADER_CONNECTIONS"     : "Connexions"
-
-    },
-
     "SETTINGS_CONNECTION_HISTORY" : {
+
         "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD",
         "ACTION_SEARCH" : "@:APP.ACTION_SEARCH",
 
@@ -842,6 +851,25 @@
 
     },
 
+    "SETTINGS_CONNECTIONS" : {
+
+      "ACTION_ACKNOWLEDGE"          : "@:APP.ACTION_ACKNOWLEDGE",
+      "ACTION_NEW_CONNECTION"       : "Nouvelle Connexion",
+      "ACTION_NEW_CONNECTION_GROUP" : "Nouveau Groupe",
+      "ACTION_NEW_SHARING_PROFILE"  : "Nouveau Profil de Partage",
+
+      "DIALOG_HEADER_ERROR"          : "Erreur",
+
+      "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+      "HELP_CONNECTIONS"   : "Cliquer ou appuyer sur une connexion en dessous pour la gérer. Selon vos permissions, les connexions peuvent être ajoutées, supprimées, leur propriétés (protocole, nom d'hôte, port, etc) changées.",
+
+      "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
+
+      "SECTION_HEADER_CONNECTIONS"     : "Connexions"
+
+    },
+
     "SETTINGS_PREFERENCES" : {
 
         "ACTION_ACKNOWLEDGE"        : "@:APP.ACTION_ACKNOWLEDGE",
@@ -878,7 +906,7 @@
         "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT",
 
         "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Méthode de saisie par défaut",
-        "SECTION_HEADER_DEFAULT_MOUSE_MODE"   : "Mode émulation souris par défaut",
+        "SECTION_HEADER_DEFAULT_MOUSE_MODE"   : "Mode d'émulation souris par défaut",
         "SECTION_HEADER_UPDATE_PASSWORD"      : "Modifier Mot de passe"
 
     },
@@ -888,13 +916,13 @@
         "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
         "ACTION_NEW_USER"      : "Nouvel Utilisateur",
 
-        "DIALOG_HEADER_ERROR"           : "Erreur",
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
 
         "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
 
         "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_USERS" : "Cliquer ou appuyer sur un utilisateur en dessous pour le gérer. Selon vos permissions, les utilisateurs peuvent être ajoutés, supprimés, leur mot de passe changé.",
+        "HELP_USERS" : "Cliquez ou appuyez sur un utilisateur en dessous pour le gérer. Selon vos permissions, les utilisateurs peuvent être ajoutés, supprimés et leur mot de passe changé.",
 
         "SECTION_HEADER_USERS"       : "Utilisateur",
 
@@ -908,7 +936,7 @@
     "SETTINGS_USER_GROUPS" : {
 
         "ACTION_ACKNOWLEDGE"    : "@:APP.ACTION_ACKNOWLEDGE",
-        "ACTION_NEW_USER_GROUP" : "Nouveau groupe",
+        "ACTION_NEW_USER_GROUP" : "Nouveau Groupe",
 
         "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
 
@@ -924,7 +952,6 @@
 
     },
 
-
     "SETTINGS_SESSIONS" : {
 
         "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -942,12 +969,12 @@
 
         "INFO_NO_SESSIONS" : "Pas de session ouverte",
 
-        "SECTION_HEADER_SESSIONS" : "Sessions Ouvertes",
+        "SECTION_HEADER_SESSIONS" : "Sessions Actives",
 
-        "TABLE_HEADER_SESSION_USERNAME"        : "Identifiant",
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nom de connexion",
         "TABLE_HEADER_SESSION_STARTDATE"       : "Ouvert depuis",
         "TABLE_HEADER_SESSION_REMOTEHOST"      : "Hôte distant",
-        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nom de connexion",
+        "TABLE_HEADER_SESSION_USERNAME"        : "Identifiant",
 
         "TEXT_CONFIRM_DELETE" : "Êtes-vous certains de vouloir fermer toutes les connexions sélectionnées ? Les utilisateurs utilisant ces sessions seront immédiatement déconnectés."
 
diff --git a/guacamole/src/main/webapp/translations/zh.json b/guacamole/src/main/webapp/translations/zh.json
index d8ad3d3..3d7d2c3 100644
--- a/guacamole/src/main/webapp/translations/zh.json
+++ b/guacamole/src/main/webapp/translations/zh.json
@@ -18,6 +18,7 @@
         "ACTION_MANAGE_SETTINGS"    : "设置",
         "ACTION_MANAGE_SESSIONS"    : "活动会话",
         "ACTION_MANAGE_USERS"       : "用户",
+        "ACTION_MANAGE_USER_GROUPS" : "用户组",
         "ACTION_NAVIGATE_BACK"      : "返回",
         "ACTION_NAVIGATE_HOME"      : "首页",
         "ACTION_SAVE"               : "保存",
@@ -28,6 +29,7 @@
 
         "DIALOG_HEADER_ERROR" : "出错",
 
+        "ERROR_PAGE_UNAVAILABLE"  : "发生错误,此操作无法完成。 如果问题仍然存在,请通知系统管理员或检查系统日志。",
         "ERROR_PASSWORD_BLANK"    : "密码不能留空。",
         "ERROR_PASSWORD_MISMATCH" : "输入的密码不吻合。",
         
@@ -77,7 +79,7 @@
         "ERROR_CLIENT_DEFAULT" : "本连接因为Guacamole服务器出现了内部错误而被终止。如果问题持续,请通知您的系统管理员,或检查您的系统日志。",
 
         "ERROR_TUNNEL_201"     : "因为正在使用的活动连接太多,Guacamole服务器拒绝了本连接。请稍后再重试。",
-        "ERROR_TUNNEL_202"     : "因服务器太久没有应答,本连续已被关闭。这通常是因为网络问题(如不稳定的无线连接或网速太慢等)而导致的。请先检查您的网络连接再重试,或者联系您的系统管理员。",
+        "ERROR_TUNNEL_202"     : "因服务器太久没有应答,本连接已被关闭。这通常是因为网络问题(如不稳定的无线连接或网速太慢等)而导致的。请先检查您的网络连接再重试,或者联系您的系统管理员。",
         "ERROR_TUNNEL_203"     : "服务器出错并关闭了本连接。请重试,或联系您的系统管理员。",
         "ERROR_TUNNEL_204"     : "请求的连接不存在。请先检查连接的名字再重试。",
         "ERROR_TUNNEL_205"     : "本连接正在使用中,并且不允许共享连接。请稍后重试。",
@@ -145,6 +147,22 @@
 
     },
 
+    "COLOR_SCHEME" : {
+
+        "ACTION_CANCEL"       : "@:APP.ACTION_CANCEL",
+        "ACTION_HIDE_DETAILS" : "隐藏",
+        "ACTION_SAVE"         : "@:APP.ACTION_SAVE",
+        "ACTION_SHOW_DETAILS" : "显示",
+
+        "FIELD_HEADER_BACKGROUND" : "背景",
+        "FIELD_HEADER_FOREGROUND" : "前景",
+
+        "FIELD_OPTION_CUSTOM" : "自定义...",
+
+        "SECTION_HEADER_DETAILS" : "详情:"
+
+    },
+
     "DATA_SOURCE_DEFAULT" : {
         "NAME" : "缺省(XML)"
     },
@@ -291,6 +309,7 @@
         "FIELD_HEADER_ADMINISTER_SYSTEM"             : "授权管理系统:",
         "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "修改自己的密码:",
         "FIELD_HEADER_CREATE_NEW_USERS"              : "新建用户:",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "新建用户组:",
         "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "新建连接:",
         "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "新建连接组:",
         "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "新建共享设定:",
@@ -300,16 +319,131 @@
 
         "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
 
+        "HELP_NO_USER_GROUPS" : "该用户当前不属于任何组。 展开此部分以添加组。",
+	
         "INFO_READ_ONLY" : "对不起,不能编辑此用户的账户。",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "没用可用的用户组.",
 
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "全部连接",
         "SECTION_HEADER_CONNECTIONS" : "连接",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "当前连接",
         "SECTION_HEADER_EDIT_USER"   : "编辑用户",
         "SECTION_HEADER_PERMISSIONS" : "使用权限",
-
+        "SECTION_HEADER_USER_GROUPS"         : "用户组",
+	
         "TEXT_CONFIRM_DELETE" : "将不能恢复已被删除的用户。确定要删除这个用户吗?"
 
     },
     
+    "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" : "删除用户组",
+        "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"               : "用户组名称:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS"        : "该组当前不属于任何组。 展开此部分以添加组。",
+        "HELP_NO_MEMBER_USER_GROUPS" : "该组当前不包含任何组。 展开此部分以添加组。",
+        "HELP_NO_MEMBER_USERS"       : "该组当前不包含任何用户。 展开此部分以添加用户。",
+
+        "INFO_READ_ONLY"                : "抱歉,无法编辑此组。",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE",
+        "INFO_NO_USERS_AVAILABLE"       : "没有可用的用户。",
+
+        "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"     : "编辑用户组",
+        "SECTION_HEADER_MEMBER_USERS"        : "会员用户",
+        "SECTION_HEADER_MEMBER_USER_GROUPS"  : "会员用户组",
+        "SECTION_HEADER_PERMISSIONS"         : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS",
+        "SECTION_HEADER_USER_GROUPS"         : "父用户组",
+
+        "TEXT_CONFIRM_DELETE" : "删除组后将无法还原。 您确定要删除该组吗?"
+
+    },
+    
+    "PROTOCOL_KUBERNETES" : {
+
+        "FIELD_HEADER_BACKSPACE"      : "发送退格键:",
+        "FIELD_HEADER_CA_CERT"         : "证书颁发机构证书:",
+        "FIELD_HEADER_CLIENT_CERT"     : "客户证书:",
+        "FIELD_HEADER_CLIENT_KEY"      : "客户端密钥:",
+        "FIELD_HEADER_COLOR_SCHEME"   : "配色方案:",
+        "FIELD_HEADER_CONTAINER"       : "容器名称:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "自动建立录像目录:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "自动建立打字稿目录:",
+        "FIELD_HEADER_FONT_NAME"      : "字体名:",
+        "FIELD_HEADER_FONT_SIZE"      : "字体大小:",
+        "FIELD_HEADER_HOSTNAME"       : "主机名:",
+        "FIELD_HEADER_IGNORE_CERT"     : "忽略服务器证书:",
+        "FIELD_HEADER_NAMESPACE"       : "命名空间:",
+        "FIELD_HEADER_POD"             : "Pod名称:",
+        "FIELD_HEADER_PORT"           : "端口:",
+        "FIELD_HEADER_READ_ONLY"      : "只读:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "排除鼠标:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "排除图像/数据流:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "包含按键事件:",
+        "FIELD_HEADER_RECORDING_NAME" : "录像名:",
+        "FIELD_HEADER_RECORDING_PATH" : "录像路径:",
+        "FIELD_HEADER_SCROLLBACK"      : "最大回滚尺寸:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "打字稿名称:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "打字稿路径:",
+        "FIELD_HEADER_USE_SSL"         : "使用SSL/TLS",
+
+        "FIELD_OPTION_BACKSPACE_EMPTY" : "",
+        "FIELD_OPTION_BACKSPACE_8"     : "退格键(Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "删除键(Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "白底黑字",
+        "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "黑底灰字",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "黑底绿字",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "黑底白字",
+
+        "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_FONT_SIZE_EMPTY" : "",
+
+        "NAME" : "Kubernetes",
+
+        "SECTION_HEADER_AUTHENTICATION" : "认证方式",
+        "SECTION_HEADER_BEHAVIOR"       : "终端行为",
+        "SECTION_HEADER_CONTAINER"      : "容器",
+        "SECTION_HEADER_DISPLAY"        : "显示",
+        "SECTION_HEADER_RECORDING"      : "屏幕录制",
+        "SECTION_HEADER_TYPESCRIPT"     : "打字稿(文本会话录制)",
+        "SECTION_HEADER_NETWORK"        : "网络"
+
+    },
+    
     "PROTOCOL_RDP" : {
 
         "FIELD_HEADER_CLIENT_NAME"     : "客户端:",
@@ -320,8 +454,11 @@
         "FIELD_HEADER_CREATE_RECORDING_PATH" : "自动建立录像目录:",
         "FIELD_HEADER_DISABLE_AUDIO"   : "禁用音频:",
         "FIELD_HEADER_DISABLE_AUTH"    : "禁用认证:",
+        "FIELD_HEADER_DISABLE_COPY"    : "禁用从远程桌面复制:",
+        "FIELD_HEADER_DISABLE_PASTE"   : "禁用从客户端粘贴:",
         "FIELD_HEADER_DOMAIN"          : "域:",
         "FIELD_HEADER_DPI"             : "分辨率(DPI):",
+        "FIELD_HEADER_DRIVE_NAME"      : "驱动器名称:",
         "FIELD_HEADER_DRIVE_PATH"      : "虚拟盘路径:",
         "FIELD_HEADER_ENABLE_AUDIO_INPUT"         : "启用音频输入(话筒):",
         "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "启用桌面合成效果(Aero):",
@@ -348,6 +485,7 @@
         "FIELD_HEADER_LOAD_BALANCE_INFO" : "负载平衡信息/cookie:",
         "FIELD_HEADER_PASSWORD"        : "密码:",
         "FIELD_HEADER_PORT"            : "端口:",
+        "FIELD_HEADER_PRINTER_NAME"    : "重定向的打印机名称:",
         "FIELD_HEADER_PRECONNECTION_BLOB" : "预连接BLOB(VM标识):",
         "FIELD_HEADER_PRECONNECTION_ID"   : "RDP源标识:",
         "FIELD_HEADER_READ_ONLY"      : "只读:",
@@ -363,6 +501,7 @@
         "FIELD_HEADER_SECURITY"        : "安全模式:",
         "FIELD_HEADER_SERVER_LAYOUT"   : "键盘布局:",
         "FIELD_HEADER_SFTP_DIRECTORY"             : "缺省文件上传目录:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "公钥(Base64):",
         "FIELD_HEADER_SFTP_HOSTNAME"              : "主机名:",
         "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive时间间隔:",
         "FIELD_HEADER_SFTP_PASSPHRASE"            : "口令:",
@@ -372,6 +511,7 @@
         "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "文件浏览器根目录:",
         "FIELD_HEADER_SFTP_USERNAME"              : "用户名:",
         "FIELD_HEADER_STATIC_CHANNELS" : "静态通道名:",
+        "FIELD_HEADER_TIMEZONE"        : "时区:",
         "FIELD_HEADER_USERNAME"        : "用户名:",
         "FIELD_HEADER_WIDTH"           : "宽度:",
 
@@ -391,6 +531,7 @@
         "FIELD_OPTION_SECURITY_RDP"   : "RDP加密",
         "FIELD_OPTION_SECURITY_TLS"   : "TLS加密",
 
+        "FIELD_OPTION_SERVER_LAYOUT_DE_CH_QWERTZ" : "Swiss German (Qwertz)",
         "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)",
         "FIELD_OPTION_SERVER_LAYOUT_EMPTY"        : "",
         "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK English (Qwerty)",
@@ -399,10 +540,12 @@
         "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE"     : "Unicode",
         "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Swiss French (Qwertz)",
         "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Hungarian (Qwertz)",       
         "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japanese (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portuguese Brazilian (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Danish (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turkish-Q (Qwerty)",
 
         "NAME" : "RDP",
@@ -430,15 +573,20 @@
         "FIELD_HEADER_COMMAND"      : "运行命令:",
         "FIELD_HEADER_CREATE_RECORDING_PATH" : "自动建立录像目录:",
         "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "自动建立打字稿目录:",
+        "FIELD_HEADER_DISABLE_COPY"  : "禁用从终端复制:",
+        "FIELD_HEADER_DISABLE_PASTE" : "禁用从客户端粘贴:",
         "FIELD_HEADER_FONT_NAME"   : "字体名:",
         "FIELD_HEADER_FONT_SIZE"   : "字体大小:",
         "FIELD_HEADER_ENABLE_SFTP" : "启用SFTP:",
+        "FIELD_HEADER_HOST_KEY"      : "公钥(Base64):",
         "FIELD_HEADER_HOSTNAME"    : "主机名:",
+        "FIELD_HEADER_LOCALE"        : "语言/地区($LANG):",
         "FIELD_HEADER_USERNAME"    : "用户名:",
         "FIELD_HEADER_PASSWORD"    : "密码:",
         "FIELD_HEADER_PASSPHRASE"  : "口令:",
         "FIELD_HEADER_PORT"        : "端口:",
         "FIELD_HEADER_PRIVATE_KEY" : "私钥:",
+        "FIELD_HEADER_SCROLLBACK"    : "最大回滚尺寸:",
         "FIELD_HEADER_READ_ONLY"   : "只读:",
         "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "排除鼠标:",
         "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "排除图像/数据流:",
@@ -446,14 +594,17 @@
         "FIELD_HEADER_RECORDING_NAME" : "录像名:",
         "FIELD_HEADER_RECORDING_PATH" : "录像路径:",
         "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "服务器keepalive时间间隔:",
+
         "FIELD_HEADER_SFTP_ROOT_DIRECTORY"   : "文件浏览器根目录:",
-        "FIELD_HEADER_TYPESCRIPT_NAME" : "打字稿名:",
+        "FIELD_HEADER_TERMINAL_TYPE"   : "终端类型:",
+        "FIELD_HEADER_TIMEZONE"        : "时区($TZ):",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "打字稿名称:",
         "FIELD_HEADER_TYPESCRIPT_PATH" : "打字稿路径:",
 
         "FIELD_OPTION_BACKSPACE_EMPTY" : "",
         "FIELD_OPTION_BACKSPACE_8"     : "退格键(Ctrl-H)",
         "FIELD_OPTION_BACKSPACE_127"   : "删除键(Ctrl-?)",
-
+	
         "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "白底黑字",
         "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
         "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "黑底灰字",
@@ -475,7 +626,15 @@
         "FIELD_OPTION_FONT_SIZE_72"    : "72",
         "FIELD_OPTION_FONT_SIZE_96"    : "96",
         "FIELD_OPTION_FONT_SIZE_EMPTY" : "",
-
+	
+        "FIELD_OPTION_TERMINAL_TYPE_ANSI"           : "ansi",
+        "FIELD_OPTION_TERMINAL_TYPE_EMPTY"          : "",
+        "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" : "认证",
@@ -492,29 +651,36 @@
 
     "PROTOCOL_TELNET" : {
 
-        "FIELD_HEADER_BACKSPACE"      : "退格键发送:",
-        "FIELD_HEADER_COLOR_SCHEME"   : "配色方案:",
-        "FIELD_HEADER_CREATE_RECORDING_PATH" : "自动建立录像目录:",
-        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "自动建立打字稿目录:",
-        "FIELD_HEADER_FONT_NAME"      : "字体名:",
-        "FIELD_HEADER_FONT_SIZE"      : "字体大小:",
-        "FIELD_HEADER_HOSTNAME"       : "主机名:",
-        "FIELD_HEADER_USERNAME"       : "用户名:",
-        "FIELD_HEADER_PASSWORD"       : "密码:",
-        "FIELD_HEADER_PASSWORD_REGEX" : "密码规则正则表达式:",
-        "FIELD_HEADER_PORT"           : "端口:",
-        "FIELD_HEADER_READ_ONLY"      : "只读:",
-        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "排除鼠标:",
-        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "排除图像/数据流:",
-        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "包含按键事件:",
-        "FIELD_HEADER_RECORDING_NAME" : "录像名:",
-        "FIELD_HEADER_RECORDING_PATH" : "录像路径:",
-        "FIELD_HEADER_TYPESCRIPT_NAME" : "打字稿名:",
-        "FIELD_HEADER_TYPESCRIPT_PATH" : "打字稿路径:",
+        "FIELD_HEADER_BACKSPACE"      : "发送退格键:",
+        "FIELD_HEADER_COLOR_SCHEME"   : "配色方案:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "自动创建记录路径:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "自动创建typescript路径:",
+        "FIELD_HEADER_DISABLE_COPY"   : "禁止从终端复制:",
+        "FIELD_HEADER_DISABLE_PASTE"  : "禁用从客户端粘贴:",
+        "FIELD_HEADER_FONT_NAME"      : "字体名称:",
+        "FIELD_HEADER_FONT_SIZE"      : "字体大小:",
+        "FIELD_HEADER_HOSTNAME"       : "主机名:",
+        "FIELD_HEADER_LOGIN_FAILURE_REGEX" : "登录失败正则表达式:",
+        "FIELD_HEADER_LOGIN_SUCCESS_REGEX" : "登录成功正则表达式:",
+        "FIELD_HEADER_USERNAME"       : "用户名:",
+        "FIELD_HEADER_USERNAME_REGEX" : "用户名正则表达式:",
+        "FIELD_HEADER_PASSWORD"       : "密码:",
+        "FIELD_HEADER_PASSWORD_REGEX" : "密码正则表达式:",
+        "FIELD_HEADER_PORT"           : "端口:",
+        "FIELD_HEADER_READ_ONLY"      : "只读:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "排除鼠标:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "排除图形/流:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "包含关键事件:",
+        "FIELD_HEADER_RECORDING_NAME" : "记录名称:",
+        "FIELD_HEADER_RECORDING_PATH" : "记录路径:",
+        "FIELD_HEADER_SCROLLBACK"     : "最大回滚尺寸:",
+        "FIELD_HEADER_TERMINAL_TYPE"   : "终端类型:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "打字稿名称:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "打字稿路径:",
 
         "FIELD_OPTION_BACKSPACE_EMPTY" : "",
-        "FIELD_OPTION_BACKSPACE_8"     : "退格键(Ctrl-H)",
-        "FIELD_OPTION_BACKSPACE_127"   : "删除键(Ctrl-?)",
+        "FIELD_OPTION_BACKSPACE_8"     : "退格键(Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "删除键(Ctrl-?)",
 
         "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "白底黑字",
         "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
@@ -538,6 +704,14 @@
         "FIELD_OPTION_FONT_SIZE_96"    : "96",
         "FIELD_OPTION_FONT_SIZE_EMPTY" : "",
 
+        "FIELD_OPTION_TERMINAL_TYPE_ANSI"           : "ansi",
+        "FIELD_OPTION_TERMINAL_TYPE_EMPTY"          : "",
+        "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" : "认证",
@@ -559,6 +733,8 @@
         "FIELD_HEADER_CURSOR"           : "光标:",
         "FIELD_HEADER_DEST_HOST"        : "目标主机:",
         "FIELD_HEADER_DEST_PORT"        : "目标端口:",
+        "FIELD_HEADER_DISABLE_COPY"     : "禁用从远程桌面复制:",
+        "FIELD_HEADER_DISABLE_PASTE"    : "禁用从客户端粘贴:",
         "FIELD_HEADER_ENABLE_AUDIO"     : "启用音频:",
         "FIELD_HEADER_ENABLE_SFTP"      : "启用SFTP:",
         "FIELD_HEADER_HOSTNAME"         : "主机名:",
@@ -572,6 +748,7 @@
         "FIELD_HEADER_RECORDING_NAME" : "录像名:",
         "FIELD_HEADER_RECORDING_PATH" : "录像路径:",
         "FIELD_HEADER_SFTP_DIRECTORY"             : "缺省文件上传目录:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "公钥(Base64):",
         "FIELD_HEADER_SFTP_HOSTNAME"              : "主机名:",
         "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive时间间隔:",
         "FIELD_HEADER_SFTP_PASSPHRASE"            : "口令:",
@@ -678,6 +855,7 @@
         "FIELD_HEADER_PASSWORD_OLD"       : "当前密码:",
         "FIELD_HEADER_PASSWORD_NEW"       : "新密码:",
         "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "确认新密码:",
+        "FIELD_HEADER_TIMEZONE"           : "时区:",
         "FIELD_HEADER_USERNAME"           : "用户名:",
         
         "HELP_DEFAULT_INPUT_METHOD" : "缺省输入法决定了Guacamole如何接收键盘事件。当使用移动设备或使用IME输入时,有可能需要更改设置。本设置可在Guacamole菜单内被单个连接的设定覆盖。",
@@ -685,7 +863,7 @@
         "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_LANGUAGE"             : "在下方列表中选择Guacamole界面所使用的语言。可选用的语言决定于系统安装了什么语言。",
+        "HELP_LOCALE"             : "以下选项与用户的语言环境有关,并将影响界面各部分的显示方式。",
         "HELP_MOUSE_MODE_ABSOLUTE"  : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE",
         "HELP_MOUSE_MODE_RELATIVE"  : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE",
         "HELP_UPDATE_PASSWORD"      : "如需改变密码,请在下面输入您的当前密码与希望使用的新密码,并点击“更新密码” 。密码的改动会立即生效。",
@@ -724,6 +902,25 @@
 
     },
     
+    "SETTINGS_USER_GROUPS" : {
+
+        "ACTION_ACKNOWLEDGE"    : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER_GROUP" : "新建用户组",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USER_GROUPS" : "单击或触摸下面的组以管理该组。 根据您的访问级别,可以添加和删除组,还可以更改其成员用户和用户组。",
+
+        "SECTION_HEADER_USER_GROUPS" : "用户组",
+
+        "TABLE_HEADER_USER_GROUP_NAME" : "用户组名称"
+
+    },
+    
     "SETTINGS_SESSIONS" : {
         
         "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -769,6 +966,7 @@
         "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"