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"