From e35f3bbd6341ec14396100b0bb0651ff7aa9334e Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Sat, 25 Dec 2021 22:24:09 -0500 Subject: [PATCH 001/289] GUACAMOLE-1402: Expose Client state enum values. --- .../src/main/webapp/modules/Client.js | 83 ++++++++++++++----- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 866714162..f3ac684d5 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -32,14 +32,7 @@ Guacamole.Client = function(tunnel) { var guac_client = this; - var STATE_IDLE = 0; - var STATE_CONNECTING = 1; - var STATE_WAITING = 2; - var STATE_CONNECTED = 3; - var STATE_DISCONNECTING = 4; - var STATE_DISCONNECTED = 5; - - var currentState = STATE_IDLE; + var currentState = Guacamole.Client.State.IDLE; var currentTimestamp = 0; var pingInterval = null; @@ -132,8 +125,8 @@ Guacamole.Client = function(tunnel) { } function isConnected() { - return currentState == STATE_CONNECTED - || currentState == STATE_WAITING; + return currentState == Guacamole.Client.State.CONNECTED + || currentState == Guacamole.Client.State.WAITING; } /** @@ -1547,8 +1540,8 @@ Guacamole.Client = function(tunnel) { }); // If received first update, no longer waiting. - if (currentState === STATE_WAITING) - setState(STATE_CONNECTED); + if (currentState === Guacamole.Client.State.WAITING) + setState(Guacamole.Client.State.CONNECTED); // Call sync handler if defined if (guac_client.onsync) @@ -1653,10 +1646,10 @@ Guacamole.Client = function(tunnel) { this.disconnect = function() { // Only attempt disconnection not disconnected. - if (currentState != STATE_DISCONNECTED - && currentState != STATE_DISCONNECTING) { + if (currentState != Guacamole.Client.State.DISCONNECTED + && currentState != Guacamole.Client.State.DISCONNECTING) { - setState(STATE_DISCONNECTING); + setState(Guacamole.Client.State.DISCONNECTING); // Stop ping if (pingInterval) @@ -1665,7 +1658,7 @@ Guacamole.Client = function(tunnel) { // Send disconnect message and disconnect tunnel.sendMessage("disconnect"); tunnel.disconnect(); - setState(STATE_DISCONNECTED); + setState(Guacamole.Client.State.DISCONNECTED); } @@ -1684,13 +1677,13 @@ Guacamole.Client = function(tunnel) { */ this.connect = function(data) { - setState(STATE_CONNECTING); + setState(Guacamole.Client.State.CONNECTING); try { tunnel.connect(data); } catch (status) { - setState(STATE_IDLE); + setState(Guacamole.Client.State.IDLE); throw status; } @@ -1699,11 +1692,63 @@ Guacamole.Client = function(tunnel) { tunnel.sendMessage("nop"); }, 5000); - setState(STATE_WAITING); + setState(Guacamole.Client.State.WAITING); }; }; +/** + * All possible Guacamole Client states. + * + * @type {!Object.} + */ +Guacamole.Client.State = { + + /** + * The client is idle, with no active connection. + * + * @type number + */ + "IDLE" : 0, + + /** + * The client is in the process of establishing a connection. + * + * @type {!number} + */ + "CONNECTING" : 1, + + /** + * The client is waiting on further information or a remote server to + * establish the connection. + * + * @type {!number} + */ + "WAITING" : 2, + + /** + * The client is actively connected to a remote server. + * + * @type {!number} + */ + "CONNECTED" : 3, + + /** + * The client is in the process of disconnecting from the remote server. + * + * @type {!number} + */ + "DISCONNECTING" : 4, + + /** + * The client has completed the connection and is no longer connected. + * + * @type {!number} + */ + "DISCONNECTED" : 5 + +}; + /** * Map of all Guacamole binary raster operations to transfer functions. * From 9fe64a77b8794e976eec5b3019ae1c3a9f1b75ef Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 27 Dec 2021 09:27:07 -0500 Subject: [PATCH 002/289] GUACAMOLE-1402: Use new Client state enum in webapp. --- .../frontend/src/app/client/types/ManagedClient.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js index fb7c8474e..bb21dca13 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js +++ b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js @@ -427,25 +427,25 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', switch (clientState) { // Idle - case 0: + case Guacamole.Client.State.IDLE: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.IDLE); break; // Connecting - case 1: + case Guacamole.Client.State.CONNECTING: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.CONNECTING); break; // Connected + waiting - case 2: + case Guacamole.Client.State.WAITING: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.WAITING); break; // Connected - case 3: + case Guacamole.Client.State.CONNECTED: ManagedClientState.setConnectionState(managedClient.clientState, ManagedClientState.ConnectionState.CONNECTED); @@ -461,9 +461,9 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', ManagedClient.updateThumbnail(managedClient); break; - // Update history when disconnecting - case 4: // Disconnecting - case 5: // Disconnected + // Update history during disconnect phases + case Guacamole.Client.State.DISCONNECTING: + case Guacamole.Client.State.DISCONNECTED: ManagedClient.updateThumbnail(managedClient); break; From c2c3428cf0be150650f6a4c9706222c508b9e788 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 3 Jan 2022 15:31:47 -0500 Subject: [PATCH 003/289] GUACAMOLE-1488: Add support for configuring LDAP SSL protocol. --- .../auth/ldap/ConnectedLDAPConfiguration.java | 6 + .../auth/ldap/LDAPConnectionService.java | 123 ++++++++++++------ .../ldap/conf/DefaultLDAPConfiguration.java | 5 + .../conf/EnvironmentLDAPConfiguration.java | 8 ++ .../ldap/conf/JacksonLDAPConfiguration.java | 14 ++ .../auth/ldap/conf/LDAPConfiguration.java | 14 ++ .../ldap/conf/LDAPGuacamoleProperties.java | 8 ++ .../auth/ldap/conf/LDAPSSLProtocol.java | 87 +++++++++++++ 8 files changed, 228 insertions(+), 37 deletions(-) create mode 100644 extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPSSLProtocol.java diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java index 5617bb785..493cafcb7 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConnectedLDAPConfiguration.java @@ -27,6 +27,7 @@ import org.apache.directory.ldap.client.api.LdapNetworkConnection; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.ldap.conf.EncryptionMethod; import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; +import org.apache.guacamole.auth.ldap.conf.LDAPSSLProtocol; import org.apache.guacamole.auth.ldap.conf.MemberAttributeType; /** @@ -161,6 +162,11 @@ public class ConnectedLDAPConfiguration implements LDAPConfiguration, AutoClosea public EncryptionMethod getEncryptionMethod() throws GuacamoleException { return config.getEncryptionMethod(); } + + @Override + public LDAPSSLProtocol getSslProtocol() throws GuacamoleException { + return config.getSslProtocol(); + } @Override public int getMaxResults() throws GuacamoleException { diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java index b8f5d30ad..06f83b5eb 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java @@ -35,6 +35,7 @@ import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleUnsupportedException; import org.apache.guacamole.auth.ldap.conf.EncryptionMethod; import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration; +import org.apache.guacamole.auth.ldap.conf.LDAPSSLProtocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,7 +52,8 @@ public class LDAPConnectionService { /** * Creates a new instance of LdapNetworkConnection, configured as required * to use the given encryption method to communicate with the LDAP server - * at the given hostname and port. The returned LdapNetworkConnection is + * at the given hostname and port, with the specified encryption method, + * SSL protocol version, and timeout. The returned LdapNetworkConnection is * configured for use but is not yet connected nor bound to the LDAP * server. It will not be bound until a bind operation is explicitly * requested, and will not be connected until it is used in an LDAP @@ -66,6 +68,85 @@ public class LDAPConnectionService { * @param encryptionMethod * The encryption method that should be used to communicate with the * LDAP server. + * + * @param sslProtocol + * The SSL protocol version to use to make a secure LDAP configuration, + * if SSL or STARTTLS is used. + * + * @param timeout + * The maximum number of milliseconds to wait for a response from the + * LDAP server. + * + * @return + * A new instance of LdapNetworkConnection which uses the given + * encryption method to communicate with the LDAP server at the given + * hostname and port. + * + * @throws GuacamoleException + * If the requested encryption method is actually not implemented (a + * bug). + */ + private LdapNetworkConnection createLDAPConnection(String host, int port, + EncryptionMethod encryptionMethod, LDAPSSLProtocol sslProtocol, + int timeout) + throws GuacamoleException { + + LdapConnectionConfig config = new LdapConnectionConfig(); + config.setLdapHost(host); + config.setLdapPort(port); + config.setTimeout(timeout); + + // Map encryption method to proper connection and socket factory + switch (encryptionMethod) { + + // Unencrypted LDAP connection + case NONE: + logger.debug("Connection to LDAP server without encryption."); + break; + + // LDAP over SSL (LDAPS) + case SSL: + logger.debug("Connecting to LDAP server using SSL/TLS."); + config.setUseSsl(true); + config.setSslProtocol(sslProtocol.toString()); + break; + + // LDAP + STARTTLS + case STARTTLS: + logger.debug("Connecting to LDAP server using STARTTLS."); + config.setUseTls(true); + config.setSslProtocol(sslProtocol.toString()); + break; + + // The encryption method, though known, is not actually + // implemented. If encountered, this would be a bug. + default: + throw new GuacamoleUnsupportedException("Unimplemented encryption method: " + encryptionMethod); + + } + + return new LdapNetworkConnection(config); + + } + + /** + * Creates a new instance of LdapNetworkConnection, configured as required + * to use the given encryption method to communicate with the LDAP server + * at the given hostname and port with the encryption method and timeout + * specified, as well. The returned LdapNetworkConnection is configured + * for use but is not yet connected nor bound to the LDAP server. It will + * not be bound until a bind operation is explicitly requested, and will + * not be connected until it is used in an LDAP operation (such as a bind). + * + * @param host + * The hostname or IP address of the LDAP server. + * + * @param port + * The TCP port that the LDAP server is listening on. + * + * @param encryptionMethod + * The encryption method that should be used to communicate with the + * LDAP server. * * @param timeout * The maximum number of milliseconds to wait for a response from the @@ -83,41 +164,8 @@ public class LDAPConnectionService { private LdapNetworkConnection createLDAPConnection(String host, int port, EncryptionMethod encryptionMethod, int timeout) throws GuacamoleException { - - LdapConnectionConfig config = new LdapConnectionConfig(); - config.setLdapHost(host); - config.setLdapPort(port); - config.setTimeout(timeout); - - // Map encryption method to proper connection and socket factory - switch (encryptionMethod) { - - // Unencrypted LDAP connection - case NONE: - logger.debug("Connection to LDAP server without encryption."); - break; - - // LDAP over SSL (LDAPS) - case SSL: - logger.debug("Connecting to LDAP server using SSL/TLS."); - config.setUseSsl(true); - break; - - // LDAP + STARTTLS - case STARTTLS: - logger.debug("Connecting to LDAP server using STARTTLS."); - config.setUseTls(true); - break; - - // The encryption method, though known, is not actually - // implemented. If encountered, this would be a bug. - default: - throw new GuacamoleUnsupportedException("Unimplemented encryption method: " + encryptionMethod); - - } - - return new LdapNetworkConnection(config); - + return createLDAPConnection(host, port, encryptionMethod, + LDAPSSLProtocol.TLSv1_3, timeout); } /** @@ -147,6 +195,7 @@ public class LDAPConnectionService { config.getServerHostname(), config.getServerPort(), config.getEncryptionMethod(), + config.getSslProtocol(), config.getNetworkTimeout()); } @@ -217,7 +266,7 @@ public class LDAPConnectionService { port = encryptionMethod.DEFAULT_PORT; return createLDAPConnection(host, port, encryptionMethod, - config.getNetworkTimeout()); + config.getSslProtocol(), config.getNetworkTimeout()); } diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java index 28ab8ed02..cb2db9b6b 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DefaultLDAPConfiguration.java @@ -89,6 +89,11 @@ public class DefaultLDAPConfiguration implements LDAPConfiguration { public EncryptionMethod getEncryptionMethod() { return EncryptionMethod.NONE; } + + @Override + public LDAPSSLProtocol getSslProtocol() { + return LDAPSSLProtocol.TLSv1_3; + } @Override public int getMaxResults() { diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java index ae2d3cf3f..9fb44a1fa 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EnvironmentLDAPConfiguration.java @@ -136,6 +136,14 @@ public class EnvironmentLDAPConfiguration implements LDAPConfiguration { DEFAULT.getEncryptionMethod() ); } + + @Override + public LDAPSSLProtocol getSslProtocol() throws GuacamoleException { + return environment.getProperty( + LDAPGuacamoleProperties.LDAP_SSL_PROTOCOL, + DEFAULT.getSslProtocol() + ); + } @Override public int getMaxResults() throws GuacamoleException { diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java index f888908ed..01d58a3e4 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/JacksonLDAPConfiguration.java @@ -116,6 +116,14 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { */ @JsonProperty("encryption-method") private String encryptionMethod; + + /** + * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_SSL_PROTOCOL}. If + * not set within the YAML, this will be null, and will default to the value + * specified by the LDAP API library. + */ + @JsonProperty("ssl-protocol") + private String sslProtocol; /** * The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MAX_SEARCH_RESULTS}. @@ -365,6 +373,12 @@ public class JacksonLDAPConfiguration implements LDAPConfiguration { return withDefault(LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD, encryptionMethod, defaultConfig::getEncryptionMethod); } + + @Override + public LDAPSSLProtocol getSslProtocol() throws GuacamoleException { + return withDefault(LDAPGuacamoleProperties.LDAP_SSL_PROTOCOL, + sslProtocol, defaultConfig::getSslProtocol); + } @Override public int getMaxResults() throws GuacamoleException { diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java index 77eb31511..975631d02 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPConfiguration.java @@ -183,6 +183,20 @@ public interface LDAPConfiguration { * If the encryption method cannot be retrieved. */ EncryptionMethod getEncryptionMethod() throws GuacamoleException; + + /** + * Returns the SSL protocol that should be used when making a secure + * connection to the LDAP server. By default the latest available TLS + * version will be used. + * + * @return + * The SSL protocol that should be used when making a secure connection + * to the LDAP server. + * + * @throws GuacamoleException + * If the SSL protocol cannot be retrieved. + */ + LDAPSSLProtocol getSslProtocol() throws GuacamoleException; /** * Returns maximum number of results a LDAP query can return. diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java index 1db4f723c..cd0b724c0 100644 --- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java @@ -170,6 +170,14 @@ public class LDAPGuacamoleProperties { public String getName() { return "ldap-encryption-method"; } }; + + public static final EnumGuacamoleProperty LDAP_SSL_PROTOCOL = + new EnumGuacamoleProperty(LDAPSSLProtocol.class) { + + @Override + public String getName() { return "ldap-ssl-protocol"; } + + }; /** * The maximum number of results a LDAP query can return. diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPSSLProtocol.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPSSLProtocol.java new file mode 100644 index 000000000..c5b1ca88d --- /dev/null +++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPSSLProtocol.java @@ -0,0 +1,87 @@ +/* + * 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.ldap.conf; + +import org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue; + +/** + * All possible SSL protocols which may be used for secure LDAP connections. + */ +public enum LDAPSSLProtocol { + + /** + * Use SSLv3 for secure LDAP connection. + */ + @PropertyValue("SSLv3") + SSLv3("SSLv3"), + + /** + * Use original TLS for secure LDAP connection. + */ + @PropertyValue("TLS") + TLS("TLS"), + + /** + * Use TLSv1 for secure LDAP connection. + */ + @PropertyValue("TLSv1") + TLSv1("TLSv1"), + + /** + * Use TLSv1.1 for secure LDAP connection. + */ + @PropertyValue("TLSv1.1") + TLSv1_1("TLSv1.1"), + + /** + * Use TLSv1.2 for secure LDAP connection. + */ + @PropertyValue("TLSv1.2") + TLSv1_2("TLSv1.2"), + + /** + * Use TLSv1.3 for secure LDAP connection. + */ + @PropertyValue("TLSv1.3") + TLSv1_3("TLSv1.3"); + + /** + * The string value of the option to use which is ultimately what the LDAP + * API consumes to set the SSL protocol. + */ + public final String STRING_VALUE; + + /** + * Initializes this SSL protocol such that it is associated with the + * given string value. + * + * @param value + * The string value that will be associated with the enum value. + */ + private LDAPSSLProtocol(String value) { + this.STRING_VALUE = value; + } + + @Override + public String toString() { + return STRING_VALUE; + } + +} From a608fa274dc09c805310b230d92290b950a3421e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 11 Mar 2022 18:37:18 +0000 Subject: [PATCH 004/289] GUACAMOLE-1550: Allow TOTP key to be cleared by setting its generation status. The previous functionality provided two checkboxes: one for requesting that the TOTP key be cleared, and another for directly managing whether the TOTP key has been confirmed. This is confusing as checkboxes normally represent state, but the "reset" checkbox here is representing an action. Instead, both checkboxes should represent state: whether the key has been generated and whether the generated key has been confirmed. --- .../guacamole/auth/totp/user/TOTPUser.java | 48 ++++++++++--------- .../auth/totp/user/TOTPUserContext.java | 2 +- .../src/main/resources/translations/en.json | 6 +-- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java index d50e3a68c..0c6f567b8 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java @@ -45,17 +45,26 @@ public class TOTPUser extends DelegatingUser { public static final String TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME = "guac-totp-key-confirmed"; /** - * The name of the field used to trigger a reset of the TOTP data. + * The name of the user attribute defines whether the TOTP key has been + * generated for the user, regardless of whether that key has been + * confirmed. This attribute is not stored, but is instead exposed + * dynamically in lieu of exposing the actual TOTP key. */ - public static final String TOTP_KEY_SECRET_RESET_FIELD = "guac-totp-reset"; + public static final String TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME = "guac-totp-key-generated"; + + /** + * The string value used by TOTP user attributes to represent the boolean + * value "true". + */ + private static final String TRUTH_VALUE = "true"; /** * The form which contains all configurable properties for this user. */ - public static final Form TOTP_CONFIG_FORM = new Form("totp-config-form", + public static final Form TOTP_ENROLLMENT_STATUS = new Form("totp-enrollment-status", Arrays.asList( - new BooleanField(TOTP_KEY_SECRET_RESET_FIELD, "true"), - new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, "true") + new BooleanField(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME, TRUTH_VALUE), + new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, TRUTH_VALUE) ) ); @@ -86,17 +95,12 @@ public class TOTPUser extends DelegatingUser { // Create independent, mutable copy of attributes Map attributes = new HashMap<>(super.getAttributes()); - - // Protect the secret value by removing it + + // Replace secret key with simple boolean attribute representing + // whether a key has been generated at all String secret = attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME); - - // If secret is null or empty, mark the reset as true. - if (secret == null || secret.isEmpty()) - attributes.put(TOTP_KEY_SECRET_RESET_FIELD, "true"); - - // If secret has a value, mark the reset as false. - else - attributes.put(TOTP_KEY_SECRET_RESET_FIELD, "false"); + if (secret != null && !secret.isEmpty()) + attributes.put(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME, TRUTH_VALUE); return attributes; @@ -107,15 +111,13 @@ public class TOTPUser extends DelegatingUser { // Create independent, mutable copy of attributes attributes = new HashMap<>(attributes); - - // Do not expose any TOTP secret attribute outside this extension + + // Do not allow TOTP secret to be directly manipulated attributes.remove(TOTP_KEY_SECRET_ATTRIBUTE_NAME); - - // Pull off the boolean reset field - String reset = attributes.remove(TOTP_KEY_SECRET_RESET_FIELD); - - // If reset has been set to true, clear the secret. - if (reset != null && reset.equals("true")) { + + // Reset TOTP status entirely if requested + String generated = attributes.remove(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME); + if (generated != null && !generated.equals(TRUTH_VALUE)) { attributes.put(TOTP_KEY_SECRET_ATTRIBUTE_NAME, null); attributes.put(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, null); } diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java index f4785193c..a7a97b022 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java @@ -68,7 +68,7 @@ public class TOTPUserContext extends DelegatingUserContext { @Override public Collection
getUserAttributes() { Collection userAttrs = new HashSet<>(super.getUserAttributes()); - userAttrs.add(TOTPUser.TOTP_CONFIG_FORM); + userAttrs.add(TOTPUser.TOTP_ENROLLMENT_STATUS); return Collections.unmodifiableCollection(userAttrs); } diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json index 55bd69ac4..80bdaf06a 100644 --- a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json @@ -33,10 +33,10 @@ "USER_ATTRIBUTES" : { - "FIELD_HEADER_GUAC_TOTP_RESET" : "Clear TOTP secret:", - "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "TOTP key confirmed:", + "FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Secret key generated:", + "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Authentication device confirmed:", - "SECTION_HEADER_TOTP_CONFIG_FORM" : "Configure TOTP" + "SECTION_HEADER_TOTP_ENROLLMENT_STATUS" : "TOTP Enrollment Status" } From d5444a2d1aaf51e9ebdeda45e241b4f94c60efea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20V=C3=A9zina?= <8386808+webtroter@users.noreply.github.com> Date: Tue, 15 Mar 2022 20:49:09 -0400 Subject: [PATCH 005/289] GUACAMOLE-1312: Adds FR-CA keyboard layout And its French and English traductions --- .../src/main/resources/org/apache/guacamole/protocols/rdp.json | 1 + guacamole/src/main/frontend/src/translations/en.json | 1 + guacamole/src/main/frontend/src/translations/fr.json | 1 + 3 files changed, 3 insertions(+) diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json index badefb11b..e78313d79 100644 --- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json +++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json @@ -100,6 +100,7 @@ "failsafe", "fr-be-azerty", "fr-fr-azerty", + "fr-ca-qwerty", "fr-ch-qwertz", "hu-hu-qwertz", "it-it-qwerty", diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 6896d0a93..db502311d 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -582,6 +582,7 @@ "FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Latin American (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Belgian French (Azerty)", + "FIELD_OPTION_SERVER_LAYOUT_FR_CA_QWERTY" : "Canadian French (Qwerty)", "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)", diff --git a/guacamole/src/main/frontend/src/translations/fr.json b/guacamole/src/main/frontend/src/translations/fr.json index 110bbe077..3c8dc2712 100644 --- a/guacamole/src/main/frontend/src/translations/fr.json +++ b/guacamole/src/main/frontend/src/translations/fr.json @@ -550,6 +550,7 @@ "FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Latino-Américain (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Français Belge (Azerty)", + "FIELD_OPTION_SERVER_LAYOUT_FR_CA_QWERTY" : "Français Canada (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Français Suisse (Qwertz)", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Français (Azerty)", "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Hongrois (Qwertz)", From c555fe64112ff6b4212eed7582e367cb204803b4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 25 Mar 2022 21:37:17 +0000 Subject: [PATCH 006/289] GUACAMOLE-1562: Ensure "ping" response is not erroneously handled as the tunnel UUID. --- guacamole-common-js/src/main/webapp/modules/Tunnel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index 23657cddc..f300821e8 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -1094,7 +1094,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { if (tunnel.uuid === null) { // Associate tunnel UUID if received - if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE) + if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && elements.length === 1) tunnel.setUUID(elements[0]); // Tunnel is now open and UUID is available From b9e79d99c5121cc8b358c71faf810492874ad2d8 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 25 Mar 2022 21:37:53 +0000 Subject: [PATCH 007/289] GUACAMOLE-1562: Update chained tunnel UUID when underlying tunnel receives UUID. --- guacamole-common-js/src/main/webapp/modules/Tunnel.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index f300821e8..fa886f561 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -1236,12 +1236,17 @@ Guacamole.ChainedTunnel = function(tunnelChain) { tunnel.onstatechange = chained_tunnel.onstatechange; tunnel.oninstruction = chained_tunnel.oninstruction; tunnel.onerror = chained_tunnel.onerror; - tunnel.onuuid = chained_tunnel.onuuid; // Assign UUID if already known if (tunnel.uuid) chained_tunnel.setUUID(tunnel.uuid); + // Assign any future received UUIDs such that they are + // accessible from the main uuid property of the chained tunnel + tunnel.onuuid = function uuidReceived(uuid) { + chained_tunnel.setUUID(uuid); + }; + committedTunnel = tunnel; } From daadb9d7571d5ec0495fb07af98c6b51aa716e73 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 30 Mar 2022 23:55:17 +0000 Subject: [PATCH 008/289] GUACAMOLE-1571: Properly wrap upload stream JSON errors. --- .../src/main/frontend/src/app/rest/services/tunnelService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js index 6a637ff71..5f9a21a7c 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js @@ -323,7 +323,7 @@ angular.module('rest').factory('tunnelService', ['$injector', // Parse and reject with resulting JSON error else if (xhr.getResponseHeader('Content-Type') === 'application/json') - deferred.reject(angular.fromJson(xhr.responseText)); + deferred.reject(new Error(angular.fromJson(xhr.responseText))); // Warn of lack of permission of a proxy rejects the upload else if (xhr.status >= 400 && xhr.status < 500) From 915596e0d7b2e53baa8f6f888c484271f1d6e3b3 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 30 Mar 2022 23:56:42 +0000 Subject: [PATCH 009/289] GUACAMOLE-1571: Check available translations rather than hardcoding which use the default. --- .../directives/guacClientNotification.js | 112 +++++++----------- .../app/client/directives/guacFileTransfer.js | 48 +++----- .../src/app/client/services/guacTranslate.js | 82 +++++++++++++ .../client/templates/guacFileTransfer.html | 2 +- .../src/app/client/types/TranslationResult.js | 59 +++++++++ 5 files changed, 201 insertions(+), 102 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/client/services/guacTranslate.js create mode 100644 guacamole/src/main/frontend/src/app/client/types/TranslationResult.js diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js index e1a08ebc6..03344b627 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js @@ -34,16 +34,16 @@ angular.module('client').directive('guacClientNotification', [function guacClien /** * The client whose status should be displayed. - * + * * @type ManagedClient */ client : '=' - + }; directive.controller = ['$scope', '$injector', '$element', function guacClientNotificationController($scope, $injector, $element) { - + // Required types const ManagedClient = $injector.get('ManagedClient'); const ManagedClientState = $injector.get('ManagedClientState'); @@ -53,6 +53,7 @@ angular.module('client').directive('guacClientNotification', [function guacClien const $location = $injector.get('$location'); const authenticationService = $injector.get('authenticationService'); const guacClientManager = $injector.get('guacClientManager'); + const guacTranslate = $injector.get('guacTranslate'); const requestService = $injector.get('requestService'); const userPageService = $injector.get('userPageService'); @@ -65,26 +66,6 @@ angular.module('client').directive('guacClientNotification', [function guacClien */ $scope.status = false; - /** - * All client error codes handled and passed off for translation. Any error - * code not present in this list will be represented by the "DEFAULT" - * translation. - */ - const CLIENT_ERRORS = { - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0207: true, - 0x0208: true, - 0x0209: true, - 0x020A: true, - 0x020B: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - /** * All error codes for which automatic reconnection is appropriate when a * client error occurs. @@ -98,26 +79,7 @@ angular.module('client').directive('guacClientNotification', [function guacClien 0x0301: true, 0x0308: true }; - - /** - * All tunnel error codes handled and passed off for translation. Any error - * code not present in this list will be represented by the "DEFAULT" - * translation. - */ - const TUNNEL_ERRORS = { - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0204: true, - 0x0205: true, - 0x0207: true, - 0x0208: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - + /** * All error codes for which automatic reconnection is appropriate when a * tunnel error occurs. @@ -239,7 +201,7 @@ angular.module('client').directive('guacClientNotification', [function guacClien // Get any associated status code const status = $scope.client.clientState.statusCode; - // Connecting + // Connecting if (connectionState === ManagedClientState.ConnectionState.CONNECTING || connectionState === ManagedClientState.ConnectionState.WAITING) { $scope.status = { @@ -254,44 +216,58 @@ angular.module('client').directive('guacClientNotification', [function guacClien // Client error else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { - // Determine translation name of error - const errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + // Translation IDs for this error code + const errorPrefix = "CLIENT.ERROR_CLIENT_"; + const errorId = errorPrefix + status.toString(16).toUpperCase(); + const defaultErrorId = errorPrefix + "DEFAULT"; // Determine whether the reconnect countdown applies const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - // Show error status - notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : "CLIENT.ERROR_CLIENT_" + errorName - }, - countdown : countdown, - actions : actions - }); + // Use the guacTranslate service to determine if there is a translation for + // this error code; if not, use the default + guacTranslate(errorId, defaultErrorId).then( + + // Show error status + translationResult => notifyConnectionClosed({ + className : "error", + title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text : { + key : translationResult.id + }, + countdown : countdown, + actions : actions + }) + ); } // Tunnel error else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { - // Determine translation name of error - const errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + // Translation IDs for this error code + const errorPrefix = "CLIENT.ERROR_TUNNEL_"; + const errorId = errorPrefix + status.toString(16).toUpperCase(); + const defaultErrorId = errorPrefix + "DEFAULT"; // Determine whether the reconnect countdown applies const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - // Show error status - notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : "CLIENT.ERROR_TUNNEL_" + errorName - }, - countdown : countdown, - actions : actions - }); + // Use the guacTranslate service to determine if there is a translation for + // this error code; if not, use the default + guacTranslate(errorId, defaultErrorId).then( + + // Show error status + translationResult => notifyConnectionClosed({ + className : "error", + title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", + text : { + key : translationResult.id + }, + countdown : countdown, + actions : actions + }) + ); } diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js index a9c09bca5..d016a721c 100644 --- a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js +++ b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js @@ -30,7 +30,7 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe /** * The file transfer to display. - * + * * @type ManagedFileUpload|ManagedFileDownload */ transfer : '=' @@ -40,27 +40,12 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe templateUrl: 'app/client/templates/guacFileTransfer.html', controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) { + // Required services + const guacTranslate = $injector.get('guacTranslate'); + // Required types var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); - /** - * All upload error codes handled and passed off for translation. - * Any error code not present in this list will be represented by - * the "DEFAULT" translation. - */ - var UPLOAD_ERRORS = { - 0x0100: true, - 0x0201: true, - 0x0202: true, - 0x0203: true, - 0x0204: true, - 0x0205: true, - 0x0301: true, - 0x0303: true, - 0x0308: true, - 0x031D: true - }; - /** * Returns the unit string that is most appropriate for the * number of bytes transferred thus far - either 'gb', 'mb', 'kb', @@ -193,7 +178,7 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe return; // Save file - saveAs($scope.transfer.blob, $scope.transfer.filename); + saveAs($scope.transfer.blob, $scope.transfer.filename); }; @@ -210,23 +195,20 @@ angular.module('client').directive('guacFileTransfer', [function guacFileTransfe return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR; }; - /** - * Returns the text of the current error as a translation string. - * - * @returns {String} - * The name of the translation string containing the text - * associated with the current error. - */ - $scope.getErrorText = function getErrorText() { + // The translated error message for the current status code + $scope.translatedErrorMessage = ''; + + $scope.$watch('transfer.transferState.statusCode', function statusCodeChanged(statusCode) { // Determine translation name of error - var status = $scope.transfer.transferState.statusCode; - var errorName = (status in UPLOAD_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + const errorName = 'CLIENT.ERROR_UPLOAD_' + statusCode.toString(16).toUpperCase(); - // Return translation string - return 'CLIENT.ERROR_UPLOAD_' + errorName; + // Use translation string, or the default if no translation is found for this error code + guacTranslate(errorName, 'CLIENT.ERROR_UPLOAD_DEFAULT').then( + translationResult => $scope.translatedErrorMessage = translationResult.message + ); - }; + }); }] // end file transfer controller diff --git a/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js new file mode 100644 index 000000000..f280e81d7 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js @@ -0,0 +1,82 @@ +/* + * 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. + */ + +/** + * A wrapper around the angular-translate $translate service that offers a + * convenient way to fall back to a default translation if the requested + * translation is not available. + */ + angular.module('client').factory('guacTranslate', ['$injector', function guacTranslate($injector) { + + // Required services + const $q = $injector.get('$q'); + const $translate = $injector.get('$translate'); + + // Required types + const TranslationResult = $injector.get('TranslationResult'); + + /** + * Returns a promise that will be resolved with a TranslationResult containg either the + * requested ID and message (if translated), or the default ID and message if translated, + * or the literal value of `defaultTranslationId` for both the ID and message if neither + * is translated. + * + * @param {String} translationId + * The requested translation ID, which may or may not be translated. + * + * @param {Sting} defaultTranslationId + * The translation ID that will be used if no translation is found for `translationId`. + * + * @returns {Promise.} + * A promise which resolves with a TranslationResult containing the results from + * the translation attempt. + */ + function translateWithFallback(translationId, defaultTranslationId) { + const deferredTranslation = $q.defer(); + + // Attempt to translate the requested translation ID + $translate(translationId).then( + + // If the requested translation is available, use that + translation => deferredTranslation.resolve(new TranslationResult({ + id: translationId, message: translation + })), + + // Otherwise, try the default translation ID + () => $translate(defaultTranslationId).then( + + // Default translation worked, so use that + defaultTranslation => + deferredTranslation.resolve(new TranslationResult({ + id: defaultTranslationId, message: defaultTranslation + })), + + // Neither translation is available; as a fallback, return default ID for both + () => deferredTranslation.resolve(new TranslationResult({ + id: defaultTranslationId, message: defaultTranslationId + })), + ) + ); + + return deferredTranslation.promise; + }; + + return translateWithFallback; + +}]); diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html index dd96baaf4..32ead84e2 100644 --- a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html +++ b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html @@ -10,7 +10,7 @@ -

{{getErrorText() | translate}}

+

{{translatedErrorMessage}}

diff --git a/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js b/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js new file mode 100644 index 000000000..0a81511bb --- /dev/null +++ b/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * Provides the TranslationResult class used by the guacTranslate service. This class contains + * both the translated message and the translation ID that generated the message, in the case + * where it's unknown whether a translation is defined or not. + */ + angular.module('client').factory('TranslationResult', [function defineTranslationResult() { + + /** + * Object which represents the result of a translation as returned from + * the guacTranslate service. + * + * @constructor + * @param {TranslationResult|Object} [template={}] + * The object whose properties should be copied within the new + * TranslationResult. + */ + const TranslationResult = function TranslationResult(template) { + + // Use empty object by default + template = template || {}; + + /** + * The translation ID. + * + * @type {String} + */ + this.id = template.id; + + /** + * The translated message. + * + * @type {String} + */ + this.message = template.message; + + }; + + return TranslationResult; + +}]); \ No newline at end of file From f93015f295e667b6fe8ff7a9eb1910a9e1ade517 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 31 Mar 2022 00:10:13 +0000 Subject: [PATCH 010/289] GUACAMOLE-1571: Match convention for function services. --- .../src/main/frontend/src/app/client/services/guacTranslate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js index f280e81d7..c7fe8e9ff 100644 --- a/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js +++ b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js @@ -47,7 +47,7 @@ * A promise which resolves with a TranslationResult containing the results from * the translation attempt. */ - function translateWithFallback(translationId, defaultTranslationId) { + var translateWithFallback = function translateWithFallback(translationId, defaultTranslationId) { const deferredTranslation = $q.defer(); // Attempt to translate the requested translation ID From 3071bb157733a3d4c3c111967c1061a9ae1df7a9 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 8 Apr 2022 21:48:33 +0000 Subject: [PATCH 011/289] GUACAMOLE-1579: Stop including the auth token when making requests to the /api/patches endpoint. --- .../main/frontend/src/app/rest/services/patchService.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/rest/services/patchService.js b/guacamole/src/main/frontend/src/app/rest/services/patchService.js index 991a54a52..ea06e5b40 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/patchService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/patchService.js @@ -25,17 +25,16 @@ angular.module('rest').factory('patchService', ['$injector', // Required services var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); var service = {}; - + /** * Makes a request to the REST API to get the list of patches, returning * a promise that provides the array of all applicable patches if * successful. Each patch is a string of raw HTML with meta information * describing the patch operation stored within meta tags. - * + * * @returns {Promise.} * A promise which will resolve with an array of HTML patches upon * success. @@ -43,14 +42,14 @@ angular.module('rest').factory('patchService', ['$injector', service.getPatches = function getPatches() { // Retrieve all applicable HTML patches - return authenticationService.request({ + return requestService({ cache : cacheService.patches, method : 'GET', url : 'api/patches' }); }; - + return service; }]); From ed390839d76bd8a57e18bec01d3f2a3ea60ada53 Mon Sep 17 00:00:00 2001 From: Dustin Lang Date: Mon, 18 Apr 2022 14:44:27 -0400 Subject: [PATCH 012/289] GUACAMOLE-1584: add CSS ids to section headers on home page --- guacamole/src/main/frontend/src/app/home/templates/home.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/home/templates/home.html b/guacamole/src/main/frontend/src/app/home/templates/home.html index b2b3db5bd..06c0f2211 100644 --- a/guacamole/src/main/frontend/src/app/home/templates/home.html +++ b/guacamole/src/main/frontend/src/app/home/templates/home.html @@ -4,14 +4,14 @@
-

{{'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | translate}}

+

{{'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | translate}}

-

{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

+

{{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

Date: Mon, 2 May 2022 18:49:00 +0000 Subject: [PATCH 013/289] GUACAMOLE-1594: Update jackson-databind to 2.13.2.1 and bump other dependencies as needed. --- doc/licenses/jackson-2.13.1/dep-coordinates.txt | 5 ----- doc/licenses/{jackson-2.13.1 => jackson-2.13.2}/NOTICE | 0 doc/licenses/{jackson-2.13.1 => jackson-2.13.2}/README | 2 +- doc/licenses/jackson-2.13.2/dep-coordinates.txt | 4 ++++ doc/licenses/jackson-databind-2.13.2.1/NOTICE | 4 ++++ doc/licenses/jackson-databind-2.13.2.1/README | 8 ++++++++ .../jackson-databind-2.13.2.1/dep-coordinates.txt | 1 + doc/licenses/snakeyaml-1.27/README | 8 -------- doc/licenses/snakeyaml-1.27/dep-coordinates.txt | 1 - doc/licenses/snakeyaml-1.28/dep-coordinates.txt | 1 - doc/licenses/{snakeyaml-1.28 => snakeyaml-1.30}/README | 2 +- doc/licenses/snakeyaml-1.30/dep-coordinates.txt | 1 + pom.xml | 5 +++-- 13 files changed, 23 insertions(+), 19 deletions(-) delete mode 100644 doc/licenses/jackson-2.13.1/dep-coordinates.txt rename doc/licenses/{jackson-2.13.1 => jackson-2.13.2}/NOTICE (100%) rename doc/licenses/{jackson-2.13.1 => jackson-2.13.2}/README (90%) create mode 100644 doc/licenses/jackson-2.13.2/dep-coordinates.txt create mode 100644 doc/licenses/jackson-databind-2.13.2.1/NOTICE create mode 100644 doc/licenses/jackson-databind-2.13.2.1/README create mode 100644 doc/licenses/jackson-databind-2.13.2.1/dep-coordinates.txt delete mode 100644 doc/licenses/snakeyaml-1.27/README delete mode 100644 doc/licenses/snakeyaml-1.27/dep-coordinates.txt delete mode 100644 doc/licenses/snakeyaml-1.28/dep-coordinates.txt rename doc/licenses/{snakeyaml-1.28 => snakeyaml-1.30}/README (91%) create mode 100644 doc/licenses/snakeyaml-1.30/dep-coordinates.txt diff --git a/doc/licenses/jackson-2.13.1/dep-coordinates.txt b/doc/licenses/jackson-2.13.1/dep-coordinates.txt deleted file mode 100644 index f2cbd8d67..000000000 --- a/doc/licenses/jackson-2.13.1/dep-coordinates.txt +++ /dev/null @@ -1,5 +0,0 @@ -com.fasterxml.jackson.core:jackson-databind:jar:2.13.1 -com.fasterxml.jackson.core:jackson-core:jar:2.13.1 -com.fasterxml.jackson.core:jackson-annotations:jar:2.13.1 -com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:jar:2.13.1 -com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.13.1 diff --git a/doc/licenses/jackson-2.13.1/NOTICE b/doc/licenses/jackson-2.13.2/NOTICE similarity index 100% rename from doc/licenses/jackson-2.13.1/NOTICE rename to doc/licenses/jackson-2.13.2/NOTICE diff --git a/doc/licenses/jackson-2.13.1/README b/doc/licenses/jackson-2.13.2/README similarity index 90% rename from doc/licenses/jackson-2.13.1/README rename to doc/licenses/jackson-2.13.2/README index 5e5ce8eb1..1b2e7efc6 100644 --- a/doc/licenses/jackson-2.13.1/README +++ b/doc/licenses/jackson-2.13.2/README @@ -1,7 +1,7 @@ Jackson (https://github.com/FasterXML/jackson) ---------------------------------------------- - Version: 2.13.1 + Version: 2.13.2 From: 'FasterXML, LLC' (https://github.com/FasterXML) License(s): Apache v2.0 diff --git a/doc/licenses/jackson-2.13.2/dep-coordinates.txt b/doc/licenses/jackson-2.13.2/dep-coordinates.txt new file mode 100644 index 000000000..5a8f7cabc --- /dev/null +++ b/doc/licenses/jackson-2.13.2/dep-coordinates.txt @@ -0,0 +1,4 @@ +com.fasterxml.jackson.core:jackson-core:jar:2.13.2 +com.fasterxml.jackson.core:jackson-annotations:jar:2.13.2 +com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:jar:2.13.2 +com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.13.2 diff --git a/doc/licenses/jackson-databind-2.13.2.1/NOTICE b/doc/licenses/jackson-databind-2.13.2.1/NOTICE new file mode 100644 index 000000000..e0b3fa0ed --- /dev/null +++ b/doc/licenses/jackson-databind-2.13.2.1/NOTICE @@ -0,0 +1,4 @@ +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. diff --git a/doc/licenses/jackson-databind-2.13.2.1/README b/doc/licenses/jackson-databind-2.13.2.1/README new file mode 100644 index 000000000..41d421838 --- /dev/null +++ b/doc/licenses/jackson-databind-2.13.2.1/README @@ -0,0 +1,8 @@ +Jackson-databind (https://github.com/FasterXML/jackson-databind) +---------------------------------------------- + + Version: 2.13.2.1 + From: 'FasterXML, LLC' (https://github.com/FasterXML) + License(s): + Apache v2.0 + diff --git a/doc/licenses/jackson-databind-2.13.2.1/dep-coordinates.txt b/doc/licenses/jackson-databind-2.13.2.1/dep-coordinates.txt new file mode 100644 index 000000000..c35811fd8 --- /dev/null +++ b/doc/licenses/jackson-databind-2.13.2.1/dep-coordinates.txt @@ -0,0 +1 @@ +com.fasterxml.jackson.core:jackson-databind:jar:2.13.2.1 diff --git a/doc/licenses/snakeyaml-1.27/README b/doc/licenses/snakeyaml-1.27/README deleted file mode 100644 index 3fcd837d6..000000000 --- a/doc/licenses/snakeyaml-1.27/README +++ /dev/null @@ -1,8 +0,0 @@ -SnakeYAML (https://bitbucket.org/asomov/snakeyaml/) ---------------------------------------------------- - - Version: 1.27 - From: 'Andrey Somov' (https://bitbucket.org/asomov/) - License(s): - Apache v2.0 - diff --git a/doc/licenses/snakeyaml-1.27/dep-coordinates.txt b/doc/licenses/snakeyaml-1.27/dep-coordinates.txt deleted file mode 100644 index d7cbad91a..000000000 --- a/doc/licenses/snakeyaml-1.27/dep-coordinates.txt +++ /dev/null @@ -1 +0,0 @@ -org.yaml:snakeyaml:jar:1.27 diff --git a/doc/licenses/snakeyaml-1.28/dep-coordinates.txt b/doc/licenses/snakeyaml-1.28/dep-coordinates.txt deleted file mode 100644 index 614b28b37..000000000 --- a/doc/licenses/snakeyaml-1.28/dep-coordinates.txt +++ /dev/null @@ -1 +0,0 @@ -org.yaml:snakeyaml:jar:1.28 diff --git a/doc/licenses/snakeyaml-1.28/README b/doc/licenses/snakeyaml-1.30/README similarity index 91% rename from doc/licenses/snakeyaml-1.28/README rename to doc/licenses/snakeyaml-1.30/README index e89d2a8c3..6d0335f75 100644 --- a/doc/licenses/snakeyaml-1.28/README +++ b/doc/licenses/snakeyaml-1.30/README @@ -1,7 +1,7 @@ SnakeYAML (https://bitbucket.org/snakeyaml/snakeyaml/ ----------------------------------------------------- - Version: 1.28 + Version: 1.30 From: 'Andrey Somov' (https://bitbucket.org/asomov/) License(s): Apache v2.0 diff --git a/doc/licenses/snakeyaml-1.30/dep-coordinates.txt b/doc/licenses/snakeyaml-1.30/dep-coordinates.txt new file mode 100644 index 000000000..d158332f3 --- /dev/null +++ b/doc/licenses/snakeyaml-1.30/dep-coordinates.txt @@ -0,0 +1 @@ +org.yaml:snakeyaml:jar:1.30 diff --git a/pom.xml b/pom.xml index 2dd057056..2ba032085 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,8 @@ 31.0.1-jre 5.0.1 2.6.1 - 2.13.1 + 2.13.2 + 2.13.2.1 2.35 5.8.2 4.13.2 @@ -375,7 +376,7 @@ com.fasterxml.jackson.core jackson-databind - ${jackson.version} + ${jackson-databind.version} com.fasterxml.jackson.dataformat From d6db8fac7ea01d8a6a4a8c2b8bc84b66be4fb471 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 23 Jul 2021 23:45:12 -0700 Subject: [PATCH 014/289] GUACAMOLE-377: Flush frames asynchronously with requestAnimationFrame() if possible. --- .../src/main/webapp/modules/Display.js | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Display.js b/guacamole-common-js/src/main/webapp/modules/Display.js index 8baa6b7d7..be01e8353 100644 --- a/guacamole-common-js/src/main/webapp/modules/Display.js +++ b/guacamole-common-js/src/main/webapp/modules/Display.js @@ -163,10 +163,28 @@ Guacamole.Display = function() { var frames = []; /** - * Flushes all pending frames. + * The ID of the animation frame request returned by the last call to + * requestAnimationFrame(). This value will only be set if the browser + * supports requestAnimationFrame(), if a frame render is currently + * pending, and if the current browser tab is currently focused (likely to + * handle requests for animation frames). In all other cases, this will be + * null. + * + * @private + * @type {number} + */ + var inProgressFrame = null; + + /** + * Flushes all pending frames synchronously. This function will block until + * all pending frames have rendered. If a frame is currently blocked by an + * asynchronous operation like an image load, this function will return + * after reaching that operation and the flush operation will + * automamtically resume after that operation completes. + * * @private */ - function __flush_frames() { + var syncFlush = function syncFlush() { var rendered_frames = 0; @@ -185,6 +203,71 @@ Guacamole.Display = function() { // Remove rendered frames from array frames.splice(0, rendered_frames); + }; + + /** + * Flushes all pending frames asynchronously. This function returns + * immediately, relying on requestAnimationFrame() to dictate when each + * frame should be flushed. + * + * @private + */ + var asyncFlush = function asyncFlush() { + + var continueFlush = function continueFlush() { + + // We're no longer waiting to render a frame + inProgressFrame = null; + + // Nothing to do if there are no frames remaining + if (!frames.length) + return; + + // Flush the next frame only if it is ready (not awaiting + // completion of some asynchronous operation like an image load) + if (frames[0].isReady()) + frames.shift().flush(); + + // Request yet another animation frame if frames remain to be + // flushed + if (frames.length) + inProgressFrame = window.requestAnimationFrame(continueFlush); + + }; + + // Begin flushing frames if not already waiting to render a frame + if (!inProgressFrame) + inProgressFrame = window.requestAnimationFrame(continueFlush); + + }; + + // Switch from asynchronous frame handling to synchronous frame handling if + // requestAnimationFrame() is unlikely to be usable (browsers may not + // invoke the animation frame callback if the relevant tab is not focused) + window.addEventListener('blur', function switchToSyncFlush() { + if (inProgressFrame && !document.hasFocus()) { + + // Cancel pending asynchronous processing of frame ... + window.cancelAnimationFrame(inProgressFrame); + inProgressFrame = null; + + // ... and instead process it synchronously + syncFlush(); + + } + }, true); + + /** + * Flushes all pending frames. + * @private + */ + function __flush_frames() { + + if (window.requestAnimationFrame && document.hasFocus()) + asyncFlush(); + else + syncFlush(); + } /** From 93d97e8c9713bcb4f771082c2afaa50037c78ac2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 2 Sep 2021 17:34:07 -0700 Subject: [PATCH 015/289] GUACAMOLE-377: Add parameter definition and translation for "enable-gfx". --- .../main/resources/org/apache/guacamole/protocols/rdp.json | 5 +++++ guacamole/src/main/frontend/src/translations/en.json | 1 + 2 files changed, 6 insertions(+) diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json index e78313d79..77855f3db 100644 --- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json +++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json @@ -254,6 +254,11 @@ { "name" : "performance", "fields" : [ + { + "name" : "enable-gfx", + "type" : "BOOLEAN", + "options" : [ "true" ] + }, { "name" : "enable-wallpaper", "type" : "BOOLEAN", diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index db502311d..3ee46a313 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -490,6 +490,7 @@ "FIELD_HEADER_ENABLE_DRIVE" : "Enable drive:", "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Enable font smoothing (ClearType):", "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Enable full-window drag:", + "FIELD_HEADER_ENABLE_GFX" : "Enable Graphics Pipeline Extension (RemoteFX):", "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Enable menu animations:", "FIELD_HEADER_DISABLE_BITMAP_CACHING" : "Disable bitmap caching:", "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING" : "Disable off-screen caching:", From e5dccc865724efd45d871cc1bc9488cc0c9791c2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 3 Sep 2021 00:25:27 -0700 Subject: [PATCH 016/289] GUACAMOLE-377: Add JavaScript API support for tracking display render statistics. --- .../src/main/webapp/modules/Client.js | 11 +- .../src/main/webapp/modules/Display.js | 264 +++++++++++++++++- 2 files changed, 267 insertions(+), 8 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 83d89abaa..2cc9c28b1 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -840,6 +840,12 @@ Guacamole.Client = function(tunnel) { * @event * @param {!number} timestamp * The timestamp associated with the sync instruction. + * + * @param {!number} frames + * The number of frames that were considered or combined to produce the + * frame associated with this sync instruction, or zero if this value + * is not known or the remote desktop server provides no concept of + * frames. */ this.onsync = null; @@ -1530,6 +1536,7 @@ Guacamole.Client = function(tunnel) { "sync": function(parameters) { var timestamp = parseInt(parameters[0]); + var frames = parameters[1] ? parseInt(parameters[1]) : 0; // Flush display, send sync when done display.flush(function displaySyncComplete() { @@ -1547,7 +1554,7 @@ Guacamole.Client = function(tunnel) { currentTimestamp = timestamp; } - }); + }, timestamp, frames); // If received first update, no longer waiting. if (currentState === STATE_WAITING) @@ -1555,7 +1562,7 @@ Guacamole.Client = function(tunnel) { // Call sync handler if defined if (guac_client.onsync) - guac_client.onsync(timestamp); + guac_client.onsync(timestamp, frames); }, diff --git a/guacamole-common-js/src/main/webapp/modules/Display.js b/guacamole-common-js/src/main/webapp/modules/Display.js index be01e8353..374a7620d 100644 --- a/guacamole-common-js/src/main/webapp/modules/Display.js +++ b/guacamole-common-js/src/main/webapp/modules/Display.js @@ -112,6 +112,17 @@ Guacamole.Display = function() { */ this.cursorY = 0; + /** + * The number of milliseconds over which display rendering statistics + * should be gathered, dispatching {@link #onstatistics} events as those + * statistics are available. If set to zero, no statistics will be + * gathered. + * + * @default 0 + * @type {!number} + */ + this.statisticWindow = 0; + /** * Fired when the default layer (and thus the entire Guacamole display) * is resized. @@ -142,6 +153,18 @@ Guacamole.Display = function() { */ this.oncursor = null; + /** + * Fired whenever performance statistics are available for recently- + * rendered frames. This event will fire only if {@link #statisticWindow} + * is non-zero. + * + * @event + * @param {!Guacamole.Display.Statistics} stats + * An object containing general rendering performance statistics for + * the remote desktop, Guacamole server, and Guacamole client. + */ + this.onstatistics = null; + /** * The queue of all pending Tasks. Tasks will be run in order, with new * tasks added at the end of the queue and old tasks removed from the @@ -186,6 +209,10 @@ Guacamole.Display = function() { */ var syncFlush = function syncFlush() { + var localTimestamp = 0; + var remoteTimestamp = 0; + + var renderedLogicalFrames = 0; var rendered_frames = 0; // Draw all pending frames, if ready @@ -196,6 +223,10 @@ Guacamole.Display = function() { break; frame.flush(); + + localTimestamp = frame.localTimestamp; + remoteTimestamp = frame.remoteTimestamp; + renderedLogicalFrames += frame.logicalFrames; rendered_frames++; } @@ -203,6 +234,9 @@ Guacamole.Display = function() { // Remove rendered frames from array frames.splice(0, rendered_frames); + if (rendered_frames) + notifyFlushed(localTimestamp, remoteTimestamp, renderedLogicalFrames); + }; /** @@ -225,8 +259,11 @@ Guacamole.Display = function() { // Flush the next frame only if it is ready (not awaiting // completion of some asynchronous operation like an image load) - if (frames[0].isReady()) - frames.shift().flush(); + if (frames[0].isReady()) { + var frame = frames.shift(); + frame.flush(); + notifyFlushed(frame.localTimestamp, frame.remoteTimestamp, frame.logicalFrames); + } // Request yet another animation frame if frames remain to be // flushed @@ -241,6 +278,101 @@ Guacamole.Display = function() { }; + /** + * Recently-gathered display render statistics, as made available by calls + * to notifyFlushed(). The contents of this array will be trimmed to + * contain only up to {@link #statisticWindow} milliseconds of statistics. + * + * @private + * @type {Guacamole.Display.Statistics[]} + */ + var statistics = []; + + /** + * Notifies that one or more frames have been successfully rendered + * (flushed) to the display. + * + * @private + * @param {!number} localTimestamp + * The local timestamp of the point in time at which the most recent, + * flushed frame was received by the display, in milliseconds since the + * Unix Epoch. + * + * @param {!number} remoteTimestamp + * The remote timestamp of sync instruction associated with the most + * recent, flushed frame received by the display. This timestamp is in + * milliseconds, but is arbitrary, having meaning only relative to + * other timestamps in the same connection. + * + * @param {!number} logicalFrames + * The number of remote desktop frames that were flushed. + */ + var notifyFlushed = function notifyFlushed(localTimestamp, remoteTimestamp, logicalFrames) { + + // Ignore if statistics are not being gathered + if (!guac_display.statisticWindow) + return; + + var current = new Date().getTime(); + + // Find the first statistic that is still within the configured time + // window + for (var first = 0; first < statistics.length; first++) { + if (current - statistics[first].timestamp <= guac_display.statisticWindow) + break; + } + + // Remove all statistics except those within the time window + statistics.splice(0, first - 1); + + // Record statistics for latest frame + statistics.push({ + localTimestamp : localTimestamp, + remoteTimestamp : remoteTimestamp, + timestamp : current, + frames : logicalFrames + }); + + // Determine the actual time interval of the available statistics (this + // will not perfectly match the configured interval, which is an upper + // bound) + var statDuration = (statistics[statistics.length - 1].timestamp - statistics[0].timestamp) / 1000; + + // Determine the amount of time that elapsed remotely (within the + // remote desktop) + var remoteDuration = (statistics[statistics.length - 1].remoteTimestamp - statistics[0].remoteTimestamp) / 1000; + + // Calculate the number of frames that have been rendered locally + // within the configured time interval + var localFrames = statistics.length; + + // Calculate the number of frames actually received from the remote + // desktop by the Guacamole server + var remoteFrames = statistics.reduce(function sumFrames(prev, stat) { + return prev + stat.frames; + }, 0); + + // Calculate the number of frames that the Guacamole server had to + // drop or combine with other frames + var drops = statistics.reduce(function sumDrops(prev, stat) { + return prev + Math.max(0, stat.frames - 1); + }, 0); + + // Produce lag and FPS statistics from above raw measurements + var stats = new Guacamole.Display.Statistics({ + processingLag : current - localTimestamp, + desktopFps : (remoteDuration && remoteFrames) ? remoteFrames / remoteDuration : null, + clientFps : statDuration ? localFrames / statDuration : null, + serverFps : remoteDuration ? localFrames / remoteDuration : null, + dropRate : remoteDuration ? drops / remoteDuration : null + }); + + // Notify of availability of new statistics + if (guac_display.onstatistics) + guac_display.onstatistics(stats); + + }; + // Switch from asynchronous frame handling to synchronous frame handling if // requestAnimationFrame() is unlikely to be usable (browsers may not // invoke the animation frame callback if the relevant tab is not focused) @@ -281,8 +413,43 @@ Guacamole.Display = function() { * * @param {!Task[]} tasks * The set of tasks which must be executed to render this frame. + * + * @param {number} [timestamp] + * The remote timestamp of sync instruction associated with this frame. + * This timestamp is in milliseconds, but is arbitrary, having meaning + * only relative to other remote timestamps in the same connection. If + * omitted, a compatible but local timestamp will be used instead. + * + * @param {number} [logicalFrames=0] + * The number of remote desktop frames that were combined to produce + * this frame, or zero if this value is unknown or inapplicable. */ - function Frame(callback, tasks) { + var Frame = function Frame(callback, tasks, timestamp, logicalFrames) { + + /** + * The local timestamp of the point in time at which this frame was + * received by the display, in milliseconds since the Unix Epoch. + * + * @type {!number} + */ + this.localTimestamp = new Date().getTime(); + + /** + * The remote timestamp of sync instruction associated with this frame. + * This timestamp is in milliseconds, but is arbitrary, having meaning + * only relative to other remote timestamps in the same connection. + * + * @type {!number} + */ + this.remoteTimestamp = timestamp || this.localTimestamp; + + /** + * The number of remote desktop frames that were combined to produce + * this frame. If unknown or not applicable, this will be zero. + * + * @type {!number} + */ + this.logicalFrames = logicalFrames || 0; /** * Cancels rendering of this frame and all associated tasks. The @@ -337,7 +504,7 @@ Guacamole.Display = function() { }; - } + }; /** * A container for an task handler. Each operation which must be ordered @@ -514,11 +681,20 @@ Guacamole.Display = function() { * @param {function} [callback] * The function to call when this frame is flushed. This may happen * immediately, or later when blocked tasks become unblocked. + * + * @param {number} timestamp + * The remote timestamp of sync instruction associated with this frame. + * This timestamp is in milliseconds, but is arbitrary, having meaning + * only relative to other remote timestamps in the same connection. + * + * @param {number} logicalFrames + * The number of remote desktop frames that were combined to produce + * this frame. */ - this.flush = function(callback) { + this.flush = function(callback, timestamp, logicalFrames) { // Add frame, reset tasks - frames.push(new Frame(callback, tasks)); + frames.push(new Frame(callback, tasks, timestamp, logicalFrames)); tasks = []; // Attempt flush @@ -1938,3 +2114,79 @@ Guacamole.Display.VisibleLayer = function(width, height) { * @type {!number} */ Guacamole.Display.VisibleLayer.__next_id = 0; + +/** + * A set of Guacamole display performance statistics, describing the speed at + * which the remote desktop, Guacamole server, and Guacamole client are + * rendering frames. + * + * @constructor + * @param {Guacamole.Display.Statistics|Object} [template={}] + * The object whose properties should be copied within the new + * Guacamole.Display.Statistics. + */ +Guacamole.Display.Statistics = function Statistics(template) { + + template = template || {}; + + /** + * The amount of time that the Guacamole client is taking to render + * individual frames, in milliseconds, if known. If this value is unknown, + * such as if the there are insufficient frame statistics recorded to + * calculate this value, this will be null. + * + * @type {?number} + */ + this.processingLag = template.processingLag; + + /** + * The framerate of the remote desktop currently being viewed within the + * relevant Gucamole.Display, independent of Guacamole, in frames per + * second. This represents the speed at which the remote desktop is + * producing frame data for the Guacamole server to consume. If this + * value is unknown, such as if the remote desktop server does not actually + * define frame boundaries, this will be null. + * + * @type {?number} + */ + this.desktopFps = template.desktopFps; + + /** + * The rate at which the Guacamole server is generating frames for the + * Guacamole client to consume, in frames per second. If the Guacamole + * server is correctly adjusting for variance in client/browser processing + * power, this rate should closely match the client rate, and should remain + * independent of any network latency. If this value is unknown, such as if + * the there are insufficient frame statistics recorded to calculate this + * value, this will be null. + * + * @type {?number} + */ + this.serverFps = template.serverFps; + + /** + * The rate at which the Guacamole client is consuming frames generated by + * the Guacamole server, in frames per second. If the Guacamole server is + * correctly adjusting for variance in client/browser processing power, + * this rate should closely match the server rate, regardless of any + * latency on the network between the server and client. If this value is + * unknown, such as if the there are insufficient frame statistics recorded + * to calculate this value, this will be null. + * + * @type {?number} + */ + this.clientFps = template.clientFps; + + /** + * The rate at which the Guacamole server is dropping or combining frames + * received from the remote desktop server to compensate for variance in + * client/browser processing power, in frames per second. This value may + * also be non-zero if the server is compensating for variances in its own + * processing power, or relative slowness in image compression vs. the rate + * that inbound frames are received. If this value is unknown, such as if + * the remote desktop server does not actually define frame boundaries, + * this will be null. + */ + this.dropRate = template.dropRate; + +}; From 59ace6c49399c865ff2ca36f61ee937cd7099fc2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 3 Sep 2021 00:52:30 -0700 Subject: [PATCH 017/289] GUACAMOLE-377: Add extension for displaying render statistics in a toolbar. --- .../guacamole-display-statistics/.gitignore | 1 + .../guacamole-display-statistics/.ratignore | 1 + .../guacamole-display-statistics/pom.xml | 124 ++++++++++++++++++ .../src/main/assembly/dist.xml | 53 ++++++++ .../directives/guacClientStatistics.js | 108 +++++++++++++++ .../src/main/resources/guac-manifest.json | 28 ++++ .../main/resources/html/add-statistics.html | 4 + .../src/main/resources/license.txt | 18 +++ .../resources/styles/clientStatistics.css | 57 ++++++++ .../templates/guacClientStatistics.html | 39 ++++++ .../src/main/resources/translations/en.json | 12 ++ extensions/pom.xml | 3 + 12 files changed, 448 insertions(+) create mode 100644 extensions/guacamole-display-statistics/.gitignore create mode 100644 extensions/guacamole-display-statistics/.ratignore create mode 100644 extensions/guacamole-display-statistics/pom.xml create mode 100644 extensions/guacamole-display-statistics/src/main/assembly/dist.xml create mode 100644 extensions/guacamole-display-statistics/src/main/resources/directives/guacClientStatistics.js create mode 100644 extensions/guacamole-display-statistics/src/main/resources/guac-manifest.json create mode 100644 extensions/guacamole-display-statistics/src/main/resources/html/add-statistics.html create mode 100644 extensions/guacamole-display-statistics/src/main/resources/license.txt create mode 100644 extensions/guacamole-display-statistics/src/main/resources/styles/clientStatistics.css create mode 100644 extensions/guacamole-display-statistics/src/main/resources/templates/guacClientStatistics.html create mode 100644 extensions/guacamole-display-statistics/src/main/resources/translations/en.json diff --git a/extensions/guacamole-display-statistics/.gitignore b/extensions/guacamole-display-statistics/.gitignore new file mode 100644 index 000000000..e55f47f1f --- /dev/null +++ b/extensions/guacamole-display-statistics/.gitignore @@ -0,0 +1 @@ +src/main/resources/generated/ diff --git a/extensions/guacamole-display-statistics/.ratignore b/extensions/guacamole-display-statistics/.ratignore new file mode 100644 index 000000000..da318d12f --- /dev/null +++ b/extensions/guacamole-display-statistics/.ratignore @@ -0,0 +1 @@ +src/main/resources/html/*.html diff --git a/extensions/guacamole-display-statistics/pom.xml b/extensions/guacamole-display-statistics/pom.xml new file mode 100644 index 000000000..403d974cf --- /dev/null +++ b/extensions/guacamole-display-statistics/pom.xml @@ -0,0 +1,124 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-display-statistics + jar + 1.4.0 + guacamole-display-statistics + http://guacamole.apache.org/ + + + org.apache.guacamole + extensions + 1.4.0 + ../ + + + + + + + + com.keithbranton.mojo + angular-maven-plugin + 0.3.4 + + + generate-resources + + html2js + + + + + ${basedir}/src/main/resources + **/*.html + ${basedir}/src/main/resources/generated/templates-main/templates.js + app/ext/display-stats + + + + + + com.github.buckelieg + minify-maven-plugin + + + default-cli + + UTF-8 + + ${basedir}/src/main/resources + ${project.build.directory}/classes + + / + / + display-stats.css + + + license.txt + + + + **/*.css + + + / + / + display-stats.js + + + license.txt + + + + **/*.js + + + + + **/*.test.js + + CLOSURE + + + + OFF + OFF + + + + + minify + + + + + + + + + diff --git a/extensions/guacamole-display-statistics/src/main/assembly/dist.xml b/extensions/guacamole-display-statistics/src/main/assembly/dist.xml new file mode 100644 index 000000000..0b16a7147 --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/assembly/dist.xml @@ -0,0 +1,53 @@ + + + + + dist + ${project.artifactId}-${project.version} + + + + tar.gz + + + + + + + + + target/licenses + + + + + target + + + *.jar + + + + + + diff --git a/extensions/guacamole-display-statistics/src/main/resources/directives/guacClientStatistics.js b/extensions/guacamole-display-statistics/src/main/resources/directives/guacClientStatistics.js new file mode 100644 index 000000000..4dc6b8a15 --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/resources/directives/guacClientStatistics.js @@ -0,0 +1,108 @@ +/* + * 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. + */ + +/** + * A directive which displays frame rendering performance statistics for a + * Guacamole client. + */ +angular.module('client').directive('guacClientStatistics', [function guacClientStatistics() { + + const directive = { + restrict: 'E', + templateUrl: 'app/ext/display-stats/templates/guacClientStatistics.html', + }; + + directive.scope = { + + /** + * The Guacamole client to display frame rendering statistics for. + * + * @type ManagedClient + */ + client : '=' + + }; + + directive.controller = ['$scope', function guacClientStatisticsController($scope) { + + /** + * Updates the displayed frame rendering statistics to the values + * within the given statistics object. + * + * @param {!Guacamole.Display.Statistics} stats + * An object containing general rendering performance statistics for + * the remote desktop, Guacamole server, and Guacamole client. + */ + var updateStatistics = function updateStatistics(stats) { + $scope.$apply(function statisticsChanged() { + $scope.statistics = stats; + }); + }; + + /** + * Returns whether the given value is a defined value that should be + * rendered within the statistics toolbar. + * + * @param {number} value + * The value to test. + * + * @returns {!boolean} + * true if the given value should be rendered within the statistics + * toolbar, false otherwise. + */ + $scope.hasValue = function hasValue(value) { + return value || value === 0; + }; + + /** + * Rounds the given numeric value to the nearest hundredth (two decimal places). + * + * @param {!number} value + * The value to round. + * + * @param {!number} + * The given value, rounded to the nearest hundredth. + */ + $scope.round = function round(value) { + return Math.round(value * 100) / 100; + }; + + // Assign/remove onstatistics handlers to track the statistics of the + // current client + $scope.$watch('client', function clientChanged(client, oldClient) { + + if (oldClient) + oldClient.managedDisplay.display.onstatistics = null; + + client.managedDisplay.display.statisticWindow = 1000; + client.managedDisplay.display.onstatistics = updateStatistics; + + }); + + // Clear onstatistics handler when directive is being unloaded + $scope.$on('$destroy', function scopeDestroyed() { + if ($scope.client) + $scope.client.managedDisplay.display.onstatistics = null; + }); + + }]; + + return directive; + +}]); diff --git a/extensions/guacamole-display-statistics/src/main/resources/guac-manifest.json b/extensions/guacamole-display-statistics/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..71c84b384 --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/resources/guac-manifest.json @@ -0,0 +1,28 @@ +{ + + "guacamoleVersion" : "1.4.0", + + "name" : "Display Statistic Toolbar", + "namespace" : "display-stats", + + "translations" : [ + "translations/en.json" + ], + + "js" : [ + "display-stats.min.js" + ], + + "css" : [ + "display-stats.min.css" + ], + + "html" : [ + "html/add-statistics.html" + ], + + "resources" : { + "templates/guacClientStatistics.html" : "text/html" + } + +} diff --git a/extensions/guacamole-display-statistics/src/main/resources/html/add-statistics.html b/extensions/guacamole-display-statistics/src/main/resources/html/add-statistics.html new file mode 100644 index 000000000..5978f7f58 --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/resources/html/add-statistics.html @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/guacamole-display-statistics/src/main/resources/license.txt b/extensions/guacamole-display-statistics/src/main/resources/license.txt new file mode 100644 index 000000000..042f3ce1f --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/resources/license.txt @@ -0,0 +1,18 @@ +/* + * 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. + */ diff --git a/extensions/guacamole-display-statistics/src/main/resources/styles/clientStatistics.css b/extensions/guacamole-display-statistics/src/main/resources/styles/clientStatistics.css new file mode 100644 index 000000000..ba4661469 --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/resources/styles/clientStatistics.css @@ -0,0 +1,57 @@ +/* + * 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. + */ + +guac-client-statistics { + font-size: 13px; + color: white; + background: #111; +} + +guac-client-statistics dl.client-statistics { + display: table; + margin: 0; + padding: 0.25em; +} + +guac-client-statistics dl.client-statistics dt, +guac-client-statistics dl.client-statistics dd { + display: table-cell; + padding: 0.25em; +} + +guac-client-statistics dl.client-statistics dt { + padding-right: 0.5em; + padding-left: 1em; +} + +guac-client-statistics dl.client-statistics dt:first-child { + padding-left: 0.5em; +} + +guac-client-statistics dl.client-statistics dd { + min-width: 6em; + border: 1px solid rgba(255, 255, 255, 0.125); + border-radius: 3px; + background: black; +} + +guac-client-statistics dl.client-statistics dd.no-value::before { + color: #888; + content: '-'; +} diff --git a/extensions/guacamole-display-statistics/src/main/resources/templates/guacClientStatistics.html b/extensions/guacamole-display-statistics/src/main/resources/templates/guacClientStatistics.html new file mode 100644 index 000000000..ae7537c0f --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/resources/templates/guacClientStatistics.html @@ -0,0 +1,39 @@ +
+ +
+ {{ 'CLIENT.FIELD_HEADER_DESKTOP_FRAMERATE' | translate }} +
+
+ +
+ +
+ {{ 'CLIENT.FIELD_HEADER_SERVER_FRAMERATE' | translate }} +
+
+ +
+ +
+ {{ 'CLIENT.FIELD_HEADER_CLIENT_FRAMERATE' | translate }} +
+
+ +
+ +
+ {{ 'CLIENT.FIELD_HEADER_DROP_FRAMERATE' | translate }} +
+
+ +
+ +
\ No newline at end of file diff --git a/extensions/guacamole-display-statistics/src/main/resources/translations/en.json b/extensions/guacamole-display-statistics/src/main/resources/translations/en.json new file mode 100644 index 000000000..ef42908e4 --- /dev/null +++ b/extensions/guacamole-display-statistics/src/main/resources/translations/en.json @@ -0,0 +1,12 @@ +{ + "CLIENT" : { + + "FIELD_HEADER_CLIENT_FRAMERATE" : "Guacamole (Client):", + "FIELD_HEADER_DESKTOP_FRAMERATE" : "Remote Desktop:", + "FIELD_HEADER_DROP_FRAMERATE" : "Drop:", + "FIELD_HEADER_SERVER_FRAMERATE" : "Guacamole (Server):", + + "INFO_FRAMERATE" : "{VALUE} fps" + + } +} diff --git a/extensions/pom.xml b/extensions/pom.xml index de2b24556..3bab33257 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -53,6 +53,9 @@ guacamole-history-recording-storage guacamole-vault + + guacamole-display-statistics + From be90de9a9dd56d4d79808fee6f5b60b525e770a3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 18 May 2022 15:40:31 -0700 Subject: [PATCH 018/289] GUACAMOLE-377: Switch over to "disable-gfx" parameter (enable RDPGFX by default). --- .../resources/org/apache/guacamole/protocols/rdp.json | 10 +++++----- guacamole/src/main/frontend/src/translations/en.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json index 77855f3db..c51f76209 100644 --- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json +++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json @@ -254,11 +254,6 @@ { "name" : "performance", "fields" : [ - { - "name" : "enable-gfx", - "type" : "BOOLEAN", - "options" : [ "true" ] - }, { "name" : "enable-wallpaper", "type" : "BOOLEAN", @@ -303,6 +298,11 @@ "name" : "disable-glyph-caching", "type" : "BOOLEAN", "options" : [ "true" ] + }, + { + "name" : "disable-gfx", + "type" : "BOOLEAN", + "options" : [ "true" ] } ] }, diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 3ee46a313..05a9fa279 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -490,11 +490,11 @@ "FIELD_HEADER_ENABLE_DRIVE" : "Enable drive:", "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Enable font smoothing (ClearType):", "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Enable full-window drag:", - "FIELD_HEADER_ENABLE_GFX" : "Enable Graphics Pipeline Extension (RemoteFX):", "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Enable menu animations:", "FIELD_HEADER_DISABLE_BITMAP_CACHING" : "Disable bitmap caching:", "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING" : "Disable off-screen caching:", "FIELD_HEADER_DISABLE_GLYPH_CACHING" : "Disable glyph caching:", + "FIELD_HEADER_DISABLE_GFX" : "Disable Graphics Pipeline Extension:", "FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:", "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", "FIELD_HEADER_ENABLE_THEMING" : "Enable theming:", From 0404bff11bd1cfc74386218dadcf9d38d4d82d88 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 24 May 2022 20:18:20 +0000 Subject: [PATCH 019/289] GUACAMOLE-1614: Upgrade to latest jasmine-maven-plugin version to avoid PhantomJS use entirely. --- guacamole-common-js/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole-common-js/pom.xml b/guacamole-common-js/pom.xml index 5d7585b18..41cc7907a 100644 --- a/guacamole-common-js/pom.xml +++ b/guacamole-common-js/pom.xml @@ -118,7 +118,7 @@ com.github.searls jasmine-maven-plugin - 2.2 + 3.0-beta-02 From cd3c1fbf3532e5b5d942d1b418aa1bb193fed1f7 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 26 May 2022 23:22:34 +0000 Subject: [PATCH 020/289] GUACAMOLE-1616: Write history records for external connections if configured to do so. --- .../jdbc/AuthenticationProviderService.java | 30 +++++ .../auth/jdbc/HistoryTrackingConnection.java | 117 ++++++++++++++++++ .../HistoryTrackingConnectionDirectory.java | 96 ++++++++++++++ .../auth/jdbc/HistoryTrackingTunnel.java | 81 ++++++++++++ .../auth/jdbc/HistoryTrackingUserContext.java | 75 +++++++++++ .../jdbc/InjectedAuthenticationProvider.java | 8 ++ .../JDBCAuthenticationProviderService.java | 31 ++++- .../guacamole/auth/jdbc/JDBCEnvironment.java | 53 +++++--- .../SharedAuthenticationProviderService.java | 9 ++ .../auth/mysql/conf/MySQLEnvironment.java | 88 +++++++------ .../mysql/conf/MySQLGuacamoleProperties.java | 59 +++++---- .../conf/PostgreSQLEnvironment.java | 82 ++++++------ .../conf/PostgreSQLGuacamoleProperties.java | 53 +++++--- .../sqlserver/conf/SQLServerEnvironment.java | 38 +++--- .../conf/SQLServerGuacamoleProperties.java | 23 +++- 15 files changed, 679 insertions(+), 164 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnectionDirectory.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingTunnel.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingUserContext.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java index afdaf332b..5fedd23e9 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/AuthenticationProviderService.java @@ -110,4 +110,34 @@ public interface AuthenticationProviderService { UserContext context, AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException; + /** + * Decorates a UserContext instance for the given already-authenticated user. + * If no decoration is required, the original UserContext will be returned. + * + * @param authenticationProvider + * The AuthenticationProvider on behalf of which the UserContext is + * being decorated. + * + * @param context + * The UserContext to decorate. + * + * @param authenticatedUser + * The AuthenticatedUser associated with the UserContext being decorated. + * + * @param credentials + * The credentials most recently submitted by the user. These + * credentials are not guaranteed to be the same as the credentials + * already associated with the AuthenticatedUser. + * + * @return + * A decorated UserContext instance for the user identified by the given + * credentials, or the original user context if no decoration is required. + * + * @throws GuacamoleException + * If the an error occurs during decoration of the UserContext. + */ + public UserContext decorateUserContext(AuthenticationProvider authenticationProvider, + UserContext context, AuthenticatedUser authenticatedUser, + Credentials credentials) throws GuacamoleException; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java new file mode 100644 index 000000000..2a0bc3cc3 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.jdbc; + +import java.util.Date; +import java.util.Map; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper; +import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel; +import org.apache.guacamole.net.GuacamoleTunnel; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.DelegatingConnection; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.protocol.GuacamoleClientInformation; + +/** + * Connection implementation that creates a history record when the connection + * is established, and returns a HistoryTrackingTunnel to automatically set the + * end date when the connection is closed. + */ +public class HistoryTrackingConnection extends DelegatingConnection { + + /** + * The current Guacamole user. + */ + private final User currentUser; + + /** + * The remote host that the user connected from. + */ + private final String remoteHost; + + /** + * The connection record mapper to use when writing history entries for + * established connections. + */ + private final ConnectionRecordMapper connectionRecordMapper; + + /** + * Creates a new HistoryConnection that wraps the given connection, + * automatically creating a history record when the connection is + * established, and returning a HistoryTrackingTunnel to set the end + * date on the history entry when the connection is closed. + * + * @param currentUser + * The current Guacamole user. + * + * @param remoteHost + * The remote host that the user connected from. + * + * @param connection + * The connection to wrap. + * + * @param connectionRecordMapper + * The connection record mapper that will be used to write the connection history records. + */ + public HistoryTrackingConnection(User currentUser, String remoteHost, Connection connection, ConnectionRecordMapper connectionRecordMapper) { + super(connection); + + this.currentUser = currentUser; + this.remoteHost = remoteHost; + this.connectionRecordMapper = connectionRecordMapper; + } + + @Override + public GuacamoleTunnel connect(GuacamoleClientInformation info, + Map tokens) throws GuacamoleException { + + // Create a connection record model, starting at the current date/time + ConnectionRecordModel connectionRecordModel = new ConnectionRecordModel(); + connectionRecordModel.setStartDate(new Date()); + + // Set the user information + connectionRecordModel.setUsername(this.currentUser.getIdentifier()); + connectionRecordModel.setRemoteHost(this.remoteHost); + + // Set the connection information + connectionRecordModel.setConnectionName(this.getDelegateConnection().getName()); + + // Insert the connection history record to mark the start of this connection + connectionRecordMapper.insert(connectionRecordModel); + + // Connect, and wrap the tunnel for return + GuacamoleTunnel tunnel = super.connect(info, tokens); + return new HistoryTrackingTunnel( + tunnel, this.connectionRecordMapper, connectionRecordModel); + } + + /** + * Get the Connection wrapped by this HistoryTrackingConnection. + * + * @return + * The wrapped Connection. + */ + public Connection getWrappedConnection() { + return getDelegateConnection(); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnectionDirectory.java new file mode 100644 index 000000000..ddc3afb19 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnectionDirectory.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.jdbc; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.DecoratingDirectory; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.User; + +/** + * A connection directory that returns HistoryTrackingConnection-wrapped connections + * when queried. + */ +public class HistoryTrackingConnectionDirectory extends DecoratingDirectory { + + /** + * The connection record mapper to use when writing history entries for + * established connections. + */ + private final ConnectionRecordMapper connectionRecordMapper; + + /** + * The user that directory operations are being performed for. + */ + private final User user; + + /** + * The remote host that the user connected from. + */ + private final String remoteHost; + + /** + * Create a new history tracking connection directory. Any connection retrieved from this + * directory will be wrapped in a HistoryTrackingConnection, enabling connection history + * records to be written with the provided connection record mapper. + * + * @param directory + * The connection directory to wrap. + * + * @param user + * The user associated with the connection directory. + * + * @param remoteHost + * The remote host that the user connected from. + * + * @param connectionRecordMapper + * The connection record mapper that will be used to write the connection history records. + */ + public HistoryTrackingConnectionDirectory(Directory directory, User user, String remoteHost, ConnectionRecordMapper connectionRecordMapper) { + super(directory); + + this.user = user; + this.remoteHost = remoteHost; + this.connectionRecordMapper = connectionRecordMapper; + } + + @Override + protected Connection decorate(Connection connection) throws GuacamoleException { + + // Wrap the connection in a history-tracking layer + return new HistoryTrackingConnection( + this.user, this.remoteHost, connection, this.connectionRecordMapper); + } + + @Override + protected Connection undecorate(Connection connection) throws GuacamoleException { + + // If the connection was wrapped, unwrap it + if (connection instanceof HistoryTrackingConnection) { + return ((HistoryTrackingConnection) connection).getWrappedConnection(); + } + + // Otherwise, return the unwrapped connection directly + return connection; + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingTunnel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingTunnel.java new file mode 100644 index 000000000..aeccc3a0b --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingTunnel.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.jdbc; + +import java.util.Date; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper; +import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel; +import org.apache.guacamole.net.DelegatingGuacamoleTunnel; +import org.apache.guacamole.net.GuacamoleTunnel; + +/** + * Tunnel implementation which automatically writes an end date for the + * provided connection history record model using the provided connection + * history mapper, when the tunnel is closed. + */ +public class HistoryTrackingTunnel extends DelegatingGuacamoleTunnel { + + /** + * The connection for which this tunnel was established. + */ + private final ConnectionRecordMapper connectionRecordMapper; + + /** + * The user for which this tunnel was established. + */ + private final ConnectionRecordModel connectionRecordModel; + + /** + * Creates a new HistoryTrackingTunnel that wraps the given tunnel, + * automatically setting the end date for the provided connection history records, + * using the provided connection history record mapper. + * + * @param tunnel + * The tunnel to wrap. + * + * @param connectionRecordMapper + * The mapper to use when writing connection history records. + * + * @param connectionRecordModel + * The connection history record model representing the in-progress connection. + */ + public HistoryTrackingTunnel(GuacamoleTunnel tunnel, + ConnectionRecordMapper connectionRecordMapper, ConnectionRecordModel connectionRecordModel) { + + super(tunnel); + + // Store the connection record mapper and model for history tracking + this.connectionRecordMapper = connectionRecordMapper; + this.connectionRecordModel = connectionRecordModel; + } + + @Override + public void close() throws GuacamoleException { + + // Set the end date to complete the connection history record + this.connectionRecordModel.setEndDate(new Date()); + this.connectionRecordMapper.updateEndDate(this.connectionRecordModel); + + super.close(); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingUserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingUserContext.java new file mode 100644 index 000000000..2c13a7ec8 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingUserContext.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.jdbc; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.DelegatingUserContext; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.UserContext; + +/** + * DelegatingUserContext implementation which writes connection history records + * when connections are established and closed. + */ +public class HistoryTrackingUserContext extends DelegatingUserContext { + + /** + * The remote host that the user associated with the user context + * connected from. + */ + private final String remoteHost; + + /** + * The connection record mapper to use when writing history entries for + * established connections. + */ + private final ConnectionRecordMapper connectionRecordMapper; + + /** + * Creates a new HistoryTrackingUserContext which wraps the given + * UserContext, allowing for tracking of connection history external to + * this authentication provider. + * + * @param userContext + * The UserContext to wrap. + * + * @param remoteHost + * The host that the user associated with the given user context connected from. + * + * @param connectionRecordMapper + * The mapper to use when writing connection history entries to the DB. + */ + public HistoryTrackingUserContext(UserContext userContext, String remoteHost, ConnectionRecordMapper connectionRecordMapper) { + super(userContext); + + this.remoteHost = remoteHost; + this.connectionRecordMapper = connectionRecordMapper; + } + + @Override + public Directory getConnectionDirectory() throws GuacamoleException { + return new HistoryTrackingConnectionDirectory( + super.getConnectionDirectory(), self(), + this.remoteHost, this.connectionRecordMapper); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java index fddb20447..8ae2eea63 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/InjectedAuthenticationProvider.java @@ -90,4 +90,12 @@ public abstract class InjectedAuthenticationProvider extends AbstractAuthenticat authenticatedUser, credentials); } + @Override + public UserContext decorate(UserContext context, + AuthenticatedUser authenticatedUser, Credentials credentials) + throws GuacamoleException { + return authProviderService.decorateUserContext(this, context, + authenticatedUser, credentials); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java index 2f38ebe23..d2576ec2b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java @@ -22,6 +22,7 @@ package org.apache.guacamole.auth.jdbc; import com.google.inject.Inject; import com.google.inject.Provider; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper; import org.apache.guacamole.auth.jdbc.security.PasswordPolicyService; import org.apache.guacamole.auth.jdbc.sharing.user.SharedAuthenticatedUser; import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser; @@ -68,6 +69,12 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider @Inject private Provider userContextProvider; + /** + * Mapper for writing connection history. + */ + @Inject + private ConnectionRecordMapper connectionRecordMapper; + @Override public AuthenticatedUser authenticateUser(AuthenticationProvider authenticationProvider, Credentials credentials) throws GuacamoleException { @@ -99,7 +106,7 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider ModeledUser user = userService.retrieveUser(authenticationProvider, authenticatedUser); ModeledUserContext context = userContextProvider.get(); if (user != null && !user.isDisabled()) { - + // Enforce applicable account restrictions if (databaseRestrictionsApplicable) { @@ -125,18 +132,18 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider } } - + // If no user account is found, and database-specific account // restrictions do not apply, get a skeleton user. else if (!databaseRestrictionsApplicable) { user = userService.retrieveSkeletonUser(authenticationProvider, authenticatedUser); - + // If auto account creation is enabled, add user to DB. if (environment.autoCreateAbsentAccounts()) { ModeledUser createdUser = userService.createObject(new PrivilegedModeledAuthenticatedUser(user.getCurrentUser()), user); user.setModel(createdUser.getModel()); } - + } // Veto authentication result only if database-specific account @@ -144,7 +151,7 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider else throw new GuacamoleInvalidCredentialsException("Invalid login", CredentialsInfo.USERNAME_PASSWORD); - + // Initialize the UserContext with the user account and return it. context.init(user.getCurrentUser()); context.recordUserLogin(); @@ -162,4 +169,18 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider } + @Override + public UserContext decorateUserContext(AuthenticationProvider authenticationProvider, + UserContext context, AuthenticatedUser authenticatedUser, + Credentials credentials) throws GuacamoleException { + + // Track connection history only for external connections, and only if enabled in the config + if (environment.trackExternalConnectionHistory() && context.getAuthenticationProvider() != authenticationProvider) { + return new HistoryTrackingUserContext(context, credentials.getRemoteHostname(), connectionRecordMapper); + } + + return context; + + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java index 19544447d..763793d62 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java @@ -30,7 +30,7 @@ import org.apache.ibatis.session.SqlSession; * intended for use within JDBC based authentication providers. */ public abstract class JDBCEnvironment extends DelegatingEnvironment { - + /** * Constructs a new JDBCEnvironment using an underlying LocalEnviroment to * read properties from the file system. @@ -68,12 +68,12 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { public abstract int getAbsoluteMaxConnections() throws GuacamoleException; /** - * Returns the default maximum number of concurrent connections to allow to - * any one connection, unless specified differently on an individual + * Returns the default maximum number of concurrent connections to allow to + * any one connection, unless specified differently on an individual * connection. Zero denotes unlimited. - * + * * @return - * The default maximum allowable number of concurrent connections + * The default maximum allowable number of concurrent connections * to any connection. * * @throws GuacamoleException @@ -82,10 +82,10 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { public abstract int getDefaultMaxConnections() throws GuacamoleException; /** - * Returns the default maximum number of concurrent connections to allow to - * any one connection group, unless specified differently on an individual + * Returns the default maximum number of concurrent connections to allow to + * any one connection group, unless specified differently on an individual * connection group. Zero denotes unlimited. - * + * * @return * The default maximum allowable number of concurrent connections * to any connection group. @@ -95,12 +95,12 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { */ public abstract int getDefaultMaxGroupConnections() throws GuacamoleException; - + /** - * Returns the default maximum number of concurrent connections to allow to + * Returns the default maximum number of concurrent connections to allow to * any one connection by an individual user, unless specified differently on * an individual connection. Zero denotes unlimited. - * + * * @return * The default maximum allowable number of concurrent connections to * any connection by an individual user. @@ -110,12 +110,12 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { */ public abstract int getDefaultMaxConnectionsPerUser() throws GuacamoleException; - + /** - * Returns the default maximum number of concurrent connections to allow to - * any one connection group by an individual user, unless specified + * Returns the default maximum number of concurrent connections to allow to + * any one connection group by an individual user, unless specified * differently on an individual connection group. Zero denotes unlimited. - * + * * @return * The default maximum allowable number of concurrent connections to * any connection group by an individual user. @@ -149,19 +149,19 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { * true if the database supports recursive queries, false otherwise. */ public abstract boolean isRecursiveQuerySupported(SqlSession session); - + /** * Returns a boolean value representing whether or not the JDBC module * should automatically create accounts within the database for users that * are successfully authenticated via other extensions. Returns true if * accounts should be auto-created, otherwise returns false. - * + * * @return * true if user accounts should be automatically created within the * database when authentication succeeds from another extension; * otherwise false. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public abstract boolean autoCreateAbsentAccounts() throws GuacamoleException; @@ -212,4 +212,19 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { } } + /** + * Returns a boolean value representing whether or not the JDBC module + * should automatically track connection history for external connections, + * i.e. connections not originated from within the JDBC auth provider + * itself. + * + * @return + * true if connection history should be tracked for connections that + * do not originate from within this JDBC auth provider, false otherwise. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public abstract boolean trackExternalConnectionHistory() throws GuacamoleException; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java index 40df1e3d3..8cd6edc36 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/SharedAuthenticationProviderService.java @@ -109,4 +109,13 @@ public class SharedAuthenticationProviderService implements AuthenticationProvid } + @Override + public UserContext decorateUserContext(AuthenticationProvider authenticationProvider, + UserContext context, AuthenticatedUser authenticatedUser, + Credentials credentials) { + + // There's no need to decorate the user context here + return context; + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java index ee3fd8aa1..d8344ec81 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java @@ -99,7 +99,7 @@ public class MySQLEnvironment extends JDBCEnvironment { * allowed to any one connection group. */ private final int DEFAULT_MAX_GROUP_CONNECTIONS = 0; - + /** * The default SSL mode for connecting to MySQL servers. */ @@ -108,8 +108,8 @@ public class MySQLEnvironment extends JDBCEnvironment { /** * Constructs a new MySQLEnvironment, providing access to MySQL-specific * configuration options. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs while setting up the underlying JDBCEnvironment * or while parsing legacy MySQL configuration options. */ @@ -177,12 +177,12 @@ public class MySQLEnvironment extends JDBCEnvironment { * database server hosting the Guacamole database. If unspecified, the * installed MySQL driver will be automatically detected by inspecting the * classes available in the classpath. - * + * * @return * The MySQL driver that will be used to communicate with the MySQL- * compatible server. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If guacamole.properties cannot be parsed, or if no MySQL-compatible * JDBC driver is present. */ @@ -210,15 +210,15 @@ public class MySQLEnvironment extends JDBCEnvironment { throw new GuacamoleServerException("No JDBC driver for MySQL/MariaDB is installed."); } - + /** * Returns the hostname of the MySQL server hosting the Guacamole * authentication tables. If unspecified, this will be "localhost". - * + * * @return * The URL of the MySQL server. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public String getMySQLHostname() throws GuacamoleException { @@ -227,30 +227,30 @@ public class MySQLEnvironment extends JDBCEnvironment { DEFAULT_HOSTNAME ); } - + /** * Returns the port number of the MySQL server hosting the Guacamole * authentication tables. If unspecified, this will be the default MySQL * port of 3306. - * + * * @return * The port number of the MySQL server. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public int getMySQLPort() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.MYSQL_PORT, DEFAULT_PORT); } - + /** - * Returns the name of the MySQL database containing the Guacamole + * Returns the name of the MySQL database containing the Guacamole * authentication tables. - * + * * @return * The name of the MySQL database. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ @@ -262,7 +262,7 @@ public class MySQLEnvironment extends JDBCEnvironment { public String getUsername() throws GuacamoleException { return getRequiredProperty(MySQLGuacamoleProperties.MYSQL_USERNAME); } - + @Override public String getPassword() throws GuacamoleException { return getRequiredProperty(MySQLGuacamoleProperties.MYSQL_PASSWORD); @@ -303,15 +303,15 @@ public class MySQLEnvironment extends JDBCEnvironment { } } - + /** * Return the MySQL SSL mode as configured in guacamole.properties, or the * default value of PREFERRED if not configured. - * + * * @return * The SSL mode to use when connecting to the MySQL server. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs retrieving the property value. */ public MySQLSSLMode getMySQLSSLMode() throws GuacamoleException { @@ -319,71 +319,71 @@ public class MySQLEnvironment extends JDBCEnvironment { MySQLGuacamoleProperties.MYSQL_SSL_MODE, DEFAULT_SSL_MODE); } - + /** * Returns the File where the trusted certificate store is located as * configured in guacamole.properties, or null if no value has been * configured. The trusted certificate store is used to validate server * certificates when making SSL connections to MySQL servers. - * + * * @return * The File where the trusted certificate store is located, or null * if the value has not been configured. - * + * * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public File getMySQLSSLTrustStore() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_TRUST_STORE); } - + /** * Returns the password used to access the trusted certificate store as * configured in guacamole.properties, or null if no password has been * specified. - * + * * @return * The password used to access the trusted certificate store. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public String getMySQLSSLTrustPassword() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_TRUST_PASSWORD); } - + /** * Returns the File used to store the client SSL certificate as configured * in guacamole.properties, or null if no value has been specified. This * file will be used to load the client certificate used for SSL connections * to MySQL servers, if the SSL connection is so configured to require * client certificate authentication. - * + * * @return * The File where the client SSL certificate is stored. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public File getMySQLSSLClientStore() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_CLIENT_STORE); } - + /** * Returns the password used to access the client certificate store as * configured in guacamole.properties, or null if no value has been * specified. - * + * * @return * The password used to access the client SSL certificate store. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If guacamole.properties cannot be parsed. */ public String getMYSQLSSLClientPassword() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_CLIENT_PASSWORD); } - + @Override public boolean autoCreateAbsentAccounts() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.MYSQL_AUTO_CREATE_ACCOUNTS, @@ -393,15 +393,23 @@ public class MySQLEnvironment extends JDBCEnvironment { /** * Return the server timezone if configured in guacamole.properties, or * null if the configuration option is not present. - * + * * @return * The server timezone as configured in guacamole.properties. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs retrieving the configuration value. */ public TimeZone getServerTimeZone() throws GuacamoleException { return getProperty(MySQLGuacamoleProperties.SERVER_TIMEZONE); } + @Override + public boolean trackExternalConnectionHistory() throws GuacamoleException { + + // Track external connection history unless explicitly disabled + return getProperty(MySQLGuacamoleProperties.MYSQL_TRACK_EXTERNAL_CONNECTION_HISTORY, + true); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java index 925f82a72..6a5944fd3 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java @@ -35,7 +35,7 @@ public class MySQLGuacamoleProperties { * This class should not be instantiated. */ private MySQLGuacamoleProperties() {} - + /** * The JDBC driver that should be used to talk to MySQL-compatible servers. */ @@ -48,7 +48,7 @@ public class MySQLGuacamoleProperties { }; /** - * The hostname of the MySQL server hosting the Guacamole authentication + * The hostname of the MySQL server hosting the Guacamole authentication * tables. */ public static final StringGuacamoleProperty MYSQL_HOSTNAME = new StringGuacamoleProperty() { @@ -59,7 +59,7 @@ public class MySQLGuacamoleProperties { }; /** - * The port number of the MySQL server hosting the Guacamole authentication + * The port number of the MySQL server hosting the Guacamole authentication * tables. */ public static final IntegerGuacamoleProperty MYSQL_PORT = new IntegerGuacamoleProperty() { @@ -70,7 +70,7 @@ public class MySQLGuacamoleProperties { }; /** - * The name of the MySQL database containing the Guacamole authentication + * The name of the MySQL database containing the Guacamole authentication * tables. */ public static final StringGuacamoleProperty MYSQL_DATABASE = new StringGuacamoleProperty() { @@ -179,19 +179,19 @@ public class MySQLGuacamoleProperties { public String getName() { return "mysql-default-max-group-connections-per-user"; } }; - + /** * The SSL mode used to connect to the MySQL Server. By default the driver * will attempt SSL connections and fall back to plain-text if SSL fails. */ public static final EnumGuacamoleProperty MYSQL_SSL_MODE = new EnumGuacamoleProperty(MySQLSSLMode.class) { - + @Override public String getName() { return "mysql-ssl-mode" ; } - + }; - + /** * The File where trusted SSL certificate authorities and server certificates * are stored. By default no file is specified, and the default Java @@ -199,24 +199,24 @@ public class MySQLGuacamoleProperties { */ public static final FileGuacamoleProperty MYSQL_SSL_TRUST_STORE = new FileGuacamoleProperty() { - + @Override public String getName() { return "mysql-ssl-trust-store"; } - + }; - + /** * The password to use to access the mysql-ssl-trust-store, if required. By * default no password will be used to attempt to access the store. */ public static final StringGuacamoleProperty MYSQL_SSL_TRUST_PASSWORD = new StringGuacamoleProperty() { - + @Override public String getName() { return "mysql-ssl-trust-password"; } - + }; - + /** * The File used to store the client certificate for configurations where * a client certificate is required for authentication. By default no @@ -224,24 +224,24 @@ public class MySQLGuacamoleProperties { */ public static final FileGuacamoleProperty MYSQL_SSL_CLIENT_STORE = new FileGuacamoleProperty() { - + @Override public String getName() { return "mysql-ssl-client-store"; } - + }; - + /** * The password to use to access the mysql-ssl-client-store file. By * default no password will be used to attempt to access the file. */ public static final StringGuacamoleProperty MYSQL_SSL_CLIENT_PASSWORD = new StringGuacamoleProperty() { - + @Override public String getName() { return "mysql-ssl-client-password"; } - + }; - + /** * Whether or not to automatically create accounts in the MySQL database for * users who successfully authenticate through another extension. By default @@ -249,7 +249,7 @@ public class MySQLGuacamoleProperties { */ public static final BooleanGuacamoleProperty MYSQL_AUTO_CREATE_ACCOUNTS = new BooleanGuacamoleProperty() { - + @Override public String getName() { return "mysql-auto-create-accounts"; } }; @@ -259,10 +259,23 @@ public class MySQLGuacamoleProperties { */ public static final TimeZoneGuacamoleProperty SERVER_TIMEZONE = new TimeZoneGuacamoleProperty() { - + @Override public String getName() { return "mysql-server-timezone"; } - + + }; + + /** + * Whether or not to track connection history for connections that do not originate + * from within the MySQL database. By default, external connection history will be + * tracked. + */ + public static final BooleanGuacamoleProperty MYSQL_TRACK_EXTERNAL_CONNECTION_HISTORY = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "mysql-track-external-connection-history"; } + }; } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java index 2ead15b6e..ac08e0ae0 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java @@ -110,7 +110,7 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { * the values that should be used in the absence of the correct properties. */ private final int DEFAULT_MAX_GROUP_CONNECTIONS = 0; - + /** * The default value to use for SSL mode if none is explicitly configured. */ @@ -119,8 +119,8 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { /** * Constructs a new PostgreSQLEnvironment, providing access to PostgreSQL-specific * configuration options. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs while setting up the underlying JDBCEnvironment * or while parsing legacy PostgreSQL configuration options. */ @@ -186,11 +186,11 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { /** * Returns the hostname of the PostgreSQL server hosting the Guacamole * authentication tables. If unspecified, this will be "localhost". - * + * * @return * The URL of the PostgreSQL server. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public String getPostgreSQLHostname() throws GuacamoleException { @@ -199,16 +199,16 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { DEFAULT_HOSTNAME ); } - + /** * Returns the port number of the PostgreSQL server hosting the Guacamole * authentication tables. If unspecified, this will be the default * PostgreSQL port of 5432. - * + * * @return * The port number of the PostgreSQL server. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public int getPostgreSQLPort() throws GuacamoleException { @@ -217,15 +217,15 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { DEFAULT_PORT ); } - + /** * Returns the name of the PostgreSQL database containing the Guacamole * authentication tables. - * + * * @return * The name of the PostgreSQL database. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ @@ -242,16 +242,16 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { public String getPassword() throws GuacamoleException { return getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_PASSWORD); } - + /** * Returns the defaultStatementTimeout set for PostgreSQL connections. * If unspecified, this will default to 0, * and should not be passed through to the backend. - * + * * @return * The statement timeout (in seconds) * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public int getPostgreSQLDefaultStatementTimeout() throws GuacamoleException { @@ -260,15 +260,15 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { DEFAULT_STATEMENT_TIMEOUT ); } - + /** * Returns the socketTimeout property to set on PostgreSQL connections. * If unspecified, this will default to 0 (no timeout) - * + * * @return * The socketTimeout to use when waiting on read operations (in seconds) * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public int getPostgreSQLSocketTimeout() throws GuacamoleException { @@ -282,85 +282,93 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { public boolean isRecursiveQuerySupported(SqlSession session) { return true; // All versions of PostgreSQL support recursive queries through CTEs } - + /** * Get the SSL mode to use to make the JDBC connection to the PostgreSQL * server. If unspecified this will default to PREFER, attempting SSL * and falling back to plain-text if SSL fails. - * + * * @return * The enum value of the SSL mode to use to make the JDBC connection * to the server. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs retrieving the value from guacamole.properties. */ public PostgreSQLSSLMode getPostgreSQLSSLMode() throws GuacamoleException { return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_MODE, DEFAULT_SSL_MODE); } - + /** * Return the SSL client certificate file to use to make the connection * to the PostgreSQL server. - * + * * @return * The SSL client certificate file to use for the PostgreSQL connection. - * + * * @throws GuacamoleException * If an error occurs retrieving the value from guacamole.properties. */ public File getPostgreSQLSSLClientCertFile() throws GuacamoleException { return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_CERT_FILE); } - + /** * Return the SSL client private key file to use to make the connection to the * PostgreSQL server. - * + * * @return * The SSL client private key file to use for the PostgreSQL connection. - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs retrieving the value from guacamole.properties. */ public File getPostgreSQLSSLClientKeyFile() throws GuacamoleException { return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_KEY_FILE); } - + /** * Return the SSL client root certificate file to use to make the connection * to the PostgreSQL server. - * + * * @return * The SSL client root certificate file to use to make the connection * to the PostgreSQL server. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs retrieving the value from guacamole.properties. */ public File getPostgreSQLSSLClientRootCertFile() throws GuacamoleException { return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_ROOT_CERT_FILE); } - + /** * Return the password to use to decrypt the private SSL key file when making * the connection to the PostgreSQL server. - * + * * @return * The password to use to decrypt the private SSL key file when making * the connection to the PostgreSQL server. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs retrieving the value from guacamole.properties. */ public String getPostgreSQLSSLClientKeyPassword() throws GuacamoleException { return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_SSL_KEY_PASSWORD); } - + @Override public boolean autoCreateAbsentAccounts() throws GuacamoleException { return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_AUTO_CREATE_ACCOUNTS, false); } - + + @Override + public boolean trackExternalConnectionHistory() throws GuacamoleException { + + // Track external connection history unless explicitly disabled + return getProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_TRACK_EXTERNAL_CONNECTION_HISTORY, + true); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java index 271d9c0dd..dfa00b68c 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java @@ -111,7 +111,7 @@ public class PostgreSQLGuacamoleProperties { * The number of seconds to wait for socket read operations. * If reading from the server takes longer than this value, the * connection will be closed. This can be used to handle network problems - * such as a dropped connection to the database. Similar to + * such as a dropped connection to the database. Similar to * postgresql-default-statement-timeout, it will have the effect of * aborting queries that take too long. * A value of 0 (the default) means the timeout is disabled. @@ -202,7 +202,7 @@ public class PostgreSQLGuacamoleProperties { public String getName() { return "postgresql-default-max-group-connections-per-user"; } }; - + /** * The SSL mode that should be used by the JDBC driver when making * connections to the remote server. By default SSL will be attempted but @@ -210,60 +210,60 @@ public class PostgreSQLGuacamoleProperties { */ public static final EnumGuacamoleProperty POSTGRESQL_SSL_MODE = new EnumGuacamoleProperty(PostgreSQLSSLMode.class) { - + @Override public String getName() { return "postgresql-ssl-mode"; } - + }; - + /** * The client SSL certificate file used by the JDBC driver to make the * SSL connection. */ public static final FileGuacamoleProperty POSTGRESQL_SSL_CERT_FILE = new FileGuacamoleProperty() { - + @Override public String getName() { return "postgresql-ssl-cert-file"; } - + }; - + /** * The client SSL private key file used by the JDBC driver to make the * SSL connection. */ public static final FileGuacamoleProperty POSTGRESQL_SSL_KEY_FILE = new FileGuacamoleProperty() { - + @Override public String getName() { return "postgresql-ssl-key-file"; } - + }; - + /** * The client SSL root certificate file used by the JDBC driver to validate * certificates when making the SSL connection. */ public static final FileGuacamoleProperty POSTGRESQL_SSL_ROOT_CERT_FILE = new FileGuacamoleProperty() { - + @Override public String getName() { return "postgresql-ssl-root-cert-file"; } - + }; - + /** * The password of the SSL private key used by the JDBC driver to make * the SSL connection to the PostgreSQL server. */ public static final StringGuacamoleProperty POSTGRESQL_SSL_KEY_PASSWORD = new StringGuacamoleProperty() { - + @Override public String getName() { return "postgresql-ssl-key-password"; } - + }; - + /** * Whether or not to automatically create accounts in the PostgreSQL * database for users who successfully authenticate through another @@ -271,10 +271,23 @@ public class PostgreSQLGuacamoleProperties { */ public static final BooleanGuacamoleProperty POSTGRESQL_AUTO_CREATE_ACCOUNTS = new BooleanGuacamoleProperty() { - + @Override public String getName() { return "postgresql-auto-create-accounts"; } - + }; - + + /** + * Whether or not to track connection history for connections that do not originate + * from within the Postgres database. By default, external connection history will be + * tracked. + */ + public static final BooleanGuacamoleProperty POSTGRESQL_TRACK_EXTERNAL_CONNECTION_HISTORY = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-track-external-connection-history"; } + + }; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java index 0e1554371..0b6983691 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java @@ -91,8 +91,8 @@ public class SQLServerEnvironment extends JDBCEnvironment { /** * Constructs a new SQLServerEnvironment, providing access to SQLServer-specific * configuration options. - * - * @throws GuacamoleException + * + * @throws GuacamoleException * If an error occurs while setting up the underlying JDBCEnvironment * or while parsing legacy SQLServer configuration options. */ @@ -158,11 +158,11 @@ public class SQLServerEnvironment extends JDBCEnvironment { /** * Returns the hostname of the SQLServer server hosting the Guacamole * authentication tables. If unspecified, this will be "localhost". - * + * * @return * The URL of the SQLServer server. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public String getSQLServerHostname() throws GuacamoleException { @@ -171,15 +171,15 @@ public class SQLServerEnvironment extends JDBCEnvironment { DEFAULT_HOSTNAME ); } - + /** * Returns the instance name of the SQL Server installation hosting the * Guacamole database, if any. If unspecified it will be null. - * + * * @return * The instance name of the SQL Server install hosting the Guacamole * database, or null if undefined. - * + * * @throws GuacamoleException * If an error occurs reading guacamole.properties. */ @@ -188,16 +188,16 @@ public class SQLServerEnvironment extends JDBCEnvironment { SQLServerGuacamoleProperties.SQLSERVER_INSTANCE ); } - + /** * Returns the port number of the SQLServer server hosting the Guacamole * authentication tables. If unspecified, this will be the default * SQLServer port of 5432. - * + * * @return * The port number of the SQLServer server. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value. */ public int getSQLServerPort() throws GuacamoleException { @@ -206,15 +206,15 @@ public class SQLServerEnvironment extends JDBCEnvironment { DEFAULT_PORT ); } - + /** * Returns the name of the SQLServer database containing the Guacamole * authentication tables. - * + * * @return * The name of the SQLServer database. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs while retrieving the property value, or if the * value was not set, as this property is required. */ @@ -226,7 +226,7 @@ public class SQLServerEnvironment extends JDBCEnvironment { public String getUsername() throws GuacamoleException { return getRequiredProperty(SQLServerGuacamoleProperties.SQLSERVER_USERNAME); } - + @Override public String getPassword() throws GuacamoleException { return getRequiredProperty(SQLServerGuacamoleProperties.SQLSERVER_PASSWORD); @@ -253,11 +253,19 @@ public class SQLServerEnvironment extends JDBCEnvironment { public boolean isRecursiveQuerySupported(SqlSession session) { return true; // All versions of SQL Server support recursive queries through CTEs } - + @Override public boolean autoCreateAbsentAccounts() throws GuacamoleException { return getProperty(SQLServerGuacamoleProperties.SQLSERVER_AUTO_CREATE_ACCOUNTS, false); } + @Override + public boolean trackExternalConnectionHistory() throws GuacamoleException { + + // Track external connection history unless explicitly disabled + return getProperty(SQLServerGuacamoleProperties.SQLSERVER_TRACK_EXTERNAL_CONNECTION_HISTORY, + true); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java index df63c53e9..432454ed8 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java @@ -44,7 +44,7 @@ public class SQLServerGuacamoleProperties { public String getName() { return "sqlserver-hostname"; } }; - + /** * The instance name of the SQL Server where the Guacamole database is running. */ @@ -53,7 +53,7 @@ public class SQLServerGuacamoleProperties { @Override public String getName() { return "sqlserver-instance"; } - + }; /** @@ -193,7 +193,7 @@ public class SQLServerGuacamoleProperties { public String getName() { return "sqlserver-driver"; } }; - + /** * Whether or not to automatically create accounts in the SQL Server * database for users who successfully authenticate through another @@ -201,10 +201,23 @@ public class SQLServerGuacamoleProperties { */ public static final BooleanGuacamoleProperty SQLSERVER_AUTO_CREATE_ACCOUNTS = new BooleanGuacamoleProperty() { - + @Override public String getName() { return "sqlserver-auto-create-accounts"; } - + + }; + + /** + * Whether or not to track connection history for connections that do not originate + * from within the SQL Server database. By default, external connection history will be + * tracked. + */ + public static final BooleanGuacamoleProperty SQLSERVER_TRACK_EXTERNAL_CONNECTION_HISTORY = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "sqlserver-track-external-connection-history"; } + }; } From 7e4627c9434e88e0ccf15f7b39ebfcfb1d6308cb Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 31 May 2022 21:36:08 +0000 Subject: [PATCH 021/289] GUACAMOLE-1616: Store HISTORY_UUID in tokens for use by other extensions. --- .../guacamole/auth/jdbc/HistoryTrackingConnection.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java index 2a0bc3cc3..de02063d7 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java @@ -20,11 +20,13 @@ package org.apache.guacamole.auth.jdbc; import java.util.Date; +import java.util.HashMap; import java.util.Map; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordMapper; import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel; +import org.apache.guacamole.auth.jdbc.connection.ModeledConnectionRecord; import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.DelegatingConnection; @@ -98,6 +100,11 @@ public class HistoryTrackingConnection extends DelegatingConnection { // Insert the connection history record to mark the start of this connection connectionRecordMapper.insert(connectionRecordModel); + // Include history record UUID as token + ModeledConnectionRecord modeledRecord = new ModeledConnectionRecord(connectionRecordModel); + Map updatedTokens = new HashMap<>(tokens); + updatedTokens.put("HISTORY_UUID", modeledRecord.getUUID().toString()); + // Connect, and wrap the tunnel for return GuacamoleTunnel tunnel = super.connect(info, tokens); return new HistoryTrackingTunnel( From 31f9107ca23618746edf2de4b13b03e33a3e3fec Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 3 Jun 2022 20:51:59 +0000 Subject: [PATCH 022/289] GUACAMOLE-1616: Send updated tokens after adding HISTORY_UUID. --- .../apache/guacamole/auth/jdbc/HistoryTrackingConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java index de02063d7..f25620bda 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/HistoryTrackingConnection.java @@ -106,7 +106,7 @@ public class HistoryTrackingConnection extends DelegatingConnection { updatedTokens.put("HISTORY_UUID", modeledRecord.getUUID().toString()); // Connect, and wrap the tunnel for return - GuacamoleTunnel tunnel = super.connect(info, tokens); + GuacamoleTunnel tunnel = super.connect(info, updatedTokens); return new HistoryTrackingTunnel( tunnel, this.connectionRecordMapper, connectionRecordModel); } From eaab38c200cfc77ff7a9ce27046e6eaaa7a99021 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 6 Jun 2022 11:20:35 -0400 Subject: [PATCH 023/289] GUACAMOLE-1606: UserGroup should retrieve UserGroup attributes, not User attributes. --- .../guacamole/rest/usergroup/UserGroupObjectTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java index 721156ac6..88000b010 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupObjectTranslator.java @@ -57,7 +57,7 @@ public class UserGroupObjectTranslator throws GuacamoleException { // Filter object attributes by defined schema - object.setAttributes(filterAttributes(userContext.getUserAttributes(), + object.setAttributes(filterAttributes(userContext.getUserGroupAttributes(), object.getAttributes())); } From 647cfa6a0c039fb00d12b3843390bb07593bb3f2 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 15 Jun 2022 22:44:32 +0000 Subject: [PATCH 024/289] GUACAMOLE-1623: Extract domain field directly from the vault, or split out of username. --- .../modules/guacamole-vault-base/pom.xml | 7 + .../vault/conf/VaultConfigurationService.java | 27 ++- .../vault/secret/WindowsUsername.java | 157 ++++++++++++++++++ .../vault/secret/WindowsUsernameTest.java | 81 +++++++++ .../ksm/conf/KsmConfigurationService.java | 17 ++ .../guacamole/vault/ksm/secret/KsmClient.java | 7 +- .../vault/ksm/secret/KsmRecordService.java | 58 ++++++- .../vault/ksm/secret/KsmSecretService.java | 49 +++++- 8 files changed, 381 insertions(+), 22 deletions(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/WindowsUsername.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml b/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml index 96fe1180a..a8137b52f 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/pom.xml @@ -59,6 +59,13 @@ jackson-dataformat-yaml
+ + + junit + junit + test + + com.google.inject diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java index a666a7b97..490da6816 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java @@ -55,7 +55,7 @@ public abstract class VaultConfigurationService { @Inject private VaultSecretService secretService; - + /** * ObjectMapper for deserializing YAML. */ @@ -127,7 +127,7 @@ public abstract class VaultConfigurationService { return Collections.emptyMap(); return mapping; - + } // Fail if YAML is invalid/unreadable @@ -169,7 +169,7 @@ public abstract class VaultConfigurationService { String secretName = super.getProperty(name); if (secretName == null) return null; - + return secretService.getValue(secretName).get(); } @@ -177,7 +177,7 @@ public abstract class VaultConfigurationService { if (e.getCause() instanceof GuacamoleException) throw (GuacamoleException) e; - + throw new GuacamoleServerException(String.format("Property " + "\"%s\" could not be retrieved from the vault.", name), e); } @@ -187,4 +187,23 @@ public abstract class VaultConfigurationService { } + /** + * Return whether Windows domains should be split out from usernames when + * fetched from the vault. + * + * For example: "DOMAIN\\user" or "user@DOMAIN" should both + * be split into seperate username and domain tokens if this configuration + * is true. If false, no domain token should be created and the above values + * should be stored directly in the username token. + * + * @return + * true if windows domains should be split out from usernames, false + * otherwise. + * + * @throws GuacamoleException + * If the value specified within guacamole.properties cannot be + * parsed. + */ + public abstract boolean getSplitWindowsUsernames() throws GuacamoleException; + } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/WindowsUsername.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/WindowsUsername.java new file mode 100644 index 000000000..36d8a1b05 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/WindowsUsername.java @@ -0,0 +1,157 @@ +/* + * 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.vault.secret; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; + +/** + * A class representing a Windows username, which may optionally also include + * a domain. This class can be used to parse the username and domain out of a + * username from a vault. + */ +public class WindowsUsername { + + /** + * A pattern for matching a down-level logon name containing a Windows + * domain and username - e.g. domain\\user. For more information, see + * https://docs.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#down-level-logon-name + */ + private static final Pattern DOWN_LEVEL_LOGON_NAME_PATTERN = Pattern.compile( + "(?[^@\\\\]+)\\\\(?[^@\\\\]+)"); + + /** + * A pattern for matching a user principal name containing a Windows + * domain and username - e.g. user@domain. For more information, see + * https://docs.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#user-principal-name + */ + private static final Pattern USER_PRINCIPAL_NAME_PATTERN = Pattern.compile( + "(?[^@\\\\]+)@(?[^@\\\\]+)"); + + /** + * The username associated with the potential Windows domain/username + * value. If no domain is found, the username field will contain the + * entire value as read from the vault. + */ + private final String username; + + /** + * The dinaun associated with the potential Windows domain/username + * value. If no domain is found, this will be null. + */ + private final String domain; + + /** + * Create a WindowsUsername record with no associated domain. + * + * @param username + * The username, which should be the entire value as extracted + * from the vault. + */ + private WindowsUsername(@Nonnull String username) { + this.username = username; + this.domain = null; + } + + /** + * Create a WindowsUsername record with a username and a domain. + * + * @param username + * The username portion of the field value from the vault. + * + * @param domain + * The domain portion of the field value from the vault. + */ + private WindowsUsername( + @Nonnull String username, @Nonnull String domain) { + this.username = username; + this.domain = domain; + } + + /** + * Return the value of the username as extracted from the vault field. + * If the domain is null, this will be the entire field value. + * + * @return + * The username value as extracted from the vault field. + */ + public String getUsername() { + return username; + } + + /** + * Return the value of the domain as extracted from the vault field. + * If this is null, it means that no domain was found in the vault field. + * + * @return + * The domain value as extracted from the vault field. + */ + public String getDomain() { + return domain; + } + + /** + * Return true if a domain was found in the vault field, false otherwise. + * + * @return + * true if a domain was found in the vault field, false otherwise. + */ + public boolean hasDomain() { + return this.domain != null; + } + + /** + * Strip off a Windows domain from the provided username, if one is + * present. For example: "DOMAIN\\user" or "user@DOMAIN" will both + * be stripped to just "user". Note: neither the '@' or '\\' characters + * are valid in Windows usernames. + * + * @param vaultField + * The raw field value as retrieved from the vault. This might contain + * a Windows domain. + * + * @return + * The provided username with the Windows domain stripped off, if one + * is present. + */ + public static WindowsUsername splitWindowsUsernameFromDomain(String vaultField) { + + // If it's the down-level logon format, return the extracted username and domain + Matcher downLevelLogonMatcher = DOWN_LEVEL_LOGON_NAME_PATTERN.matcher(vaultField); + if (downLevelLogonMatcher.matches()) + return new WindowsUsername( + downLevelLogonMatcher.group("username"), + downLevelLogonMatcher.group("domain")); + + // If it's the user principal format, return the extracted username and domain + Matcher userPrincipalMatcher = USER_PRINCIPAL_NAME_PATTERN.matcher(vaultField); + if (userPrincipalMatcher.matches()) + return new WindowsUsername( + userPrincipalMatcher.group("username"), + userPrincipalMatcher.group("domain")); + + // If none of the expected formats matched, return the username with do domain + return new WindowsUsername(vaultField); + + } + +} \ No newline at end of file diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java new file mode 100644 index 000000000..9a4c5c716 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java @@ -0,0 +1,81 @@ +/* + * 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.vault.secret; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +/** + * Class to test the parsing functionality of the WindowsUsername class. + */ +public class WindowsUsernameTest { + + /** + * Verify that the splitWindowsUsernameFromDomain() method correctly strips Windows + * domains from provided usernames that include them, and does not modify + * usernames that do not have Windows domains. + */ + @Test + public void testSplitWindowsUsernameFromDomain() { + + WindowsUsername usernameAndDomain; + + // If no Windows domain is present in the provided field, the username should + // contain the entire field, and no domain should be returned + usernameAndDomain = WindowsUsername.splitWindowsUsernameFromDomain("bob"); + assertEquals(usernameAndDomain.getUsername(), "bob"); + assertFalse(usernameAndDomain.hasDomain()); + + // It should parse down-level logon name style domains + usernameAndDomain = WindowsUsername.splitWindowsUsernameFromDomain("localhost\\bob"); + assertEquals("bob", usernameAndDomain.getUsername(), "bob"); + assertTrue(usernameAndDomain.hasDomain()); + assertEquals("localhost", usernameAndDomain.getDomain()); + + // It should parse user principal name style domains + usernameAndDomain = WindowsUsername.splitWindowsUsernameFromDomain("bob@localhost"); + assertEquals("bob", usernameAndDomain.getUsername(), "bob"); + assertTrue(usernameAndDomain.hasDomain()); + assertEquals("localhost", usernameAndDomain.getDomain()); + + // It should not match if there are an invalid number of separators + List invalidSeparators = List.of( + "bob@local@host", "local\\host\\bob", + "bob\\local@host", "local@host\\bob"); + invalidSeparators.stream().forEach( + invalidSeparator -> { + + // An invalid number of separators means that the parse failed - + // there should be no detected domain, and the entire field value + // should be returned as the username + WindowsUsername parseOutput = + WindowsUsername.splitWindowsUsernameFromDomain(invalidSeparator); + assertFalse(parseOutput.hasDomain()); + assertEquals(invalidSeparator, parseOutput.getUsername()); + + }); + + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java index 38bcaaef1..0bd3f40da 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java @@ -76,6 +76,18 @@ public class KsmConfigurationService extends VaultConfigurationService { } }; + /** + * Whether windows domains should be stripped off from usernames that are + * read from the KSM vault. + */ + private static final BooleanGuacamoleProperty STRIP_WINDOWS_DOMAINS = new BooleanGuacamoleProperty() { + + @Override + public String getName() { + return "ksm-strip-windows-domains"; + } + }; + /** * Creates a new KsmConfigurationService which reads the configuration * from "ksm-token-mapping.yml" and properties from @@ -105,6 +117,11 @@ public class KsmConfigurationService extends VaultConfigurationService { return environment.getProperty(ALLOW_UNVERIFIED_CERT, false); } + @Override + public boolean getSplitWindowsUsernames() throws GuacamoleException { + return environment.getProperty(STRIP_WINDOWS_DOMAINS, false); + } + /** * Returns the options required to authenticate with Keeper Secrets Manager * when retrieving secrets. These options are read from the contents of diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index 2372dcb29..fa177014b 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -22,7 +22,6 @@ package org.apache.guacamole.vault.ksm.secret; import com.google.inject.Inject; import com.google.inject.Singleton; import com.keepersecurity.secretsManager.core.Hosts; -import com.keepersecurity.secretsManager.core.KeeperFile; import com.keepersecurity.secretsManager.core.KeeperRecord; import com.keepersecurity.secretsManager.core.KeeperSecrets; import com.keepersecurity.secretsManager.core.Login; @@ -119,7 +118,7 @@ public class KsmClient { * {@link #cacheLock} acquired appropriately. */ private KeeperSecrets cachedSecrets = null; - + /** * All records retrieved from Keeper Secrets Manager, where each key is the * UID of the corresponding record. The contents of this Map are @@ -216,7 +215,7 @@ public class KsmClient { // Store all secrets within cache cachedSecrets = secrets; - + // Clear unambiguous cache of all records by UID cachedRecordsByUid.clear(); @@ -398,7 +397,7 @@ public class KsmClient { * The record associated with the given username, or null if there is * no such record or multiple such records. * - * @throws GuacamoleException + * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ public KeeperRecord getRecordByLogin(String username) throws GuacamoleException { diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java index d67b5816a..a3d79b49e 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmRecordService.java @@ -41,12 +41,20 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; + /** * Service for automatically parsing out secrets and data from Keeper records. */ @Singleton public class KsmRecordService { + /** + * Regular expression which matches the labels of custom fields containing + * domains. + */ + private static final Pattern DOMAIN_LABEL_PATTERN = + Pattern.compile("domain", Pattern.CASE_INSENSITIVE); + /** * Regular expression which matches the labels of custom fields containing * hostnames/addresses. @@ -143,13 +151,13 @@ public class KsmRecordService { * empty or contains multiple values, null is returned. Note that null will * also be returned if the mapping transformation returns null for the * single value stored in the list. - * + * * @param * The type of object stored in the list. - * + * * @param * The type of object to return. - * + * * @param values * The list to retrieve a single value from. * @@ -168,7 +176,7 @@ public class KsmRecordService { return null; return mapper.apply(value); - + } /** @@ -271,7 +279,7 @@ public class KsmRecordService { * multiple such fields, null is returned. Both standard and custom fields * are searched. As standard fields do not have labels, any given label * pattern is ignored for standard fields. - * + * * @param * The type of field to return. * @@ -339,7 +347,7 @@ public class KsmRecordService { return null; foundFile = file; - + } return foundFile; @@ -362,7 +370,7 @@ public class KsmRecordService { if (file == null) return CompletableFuture.completedFuture(null); - + return CompletableFuture.supplyAsync(() -> { return new String(SecretsManager.downloadFile(file), StandardCharsets.UTF_8); }); @@ -445,6 +453,38 @@ public class KsmRecordService { } + /** + * Returns the single domain associated with the given record. If the + * record has no associated domain, or multiple domains, null is + * returned. Domains are retrieved from "Text" and "Hidden" fields + * that have the label "domain" (case-insensitive). + * + * @param record + * The record to retrieve the domain from. + * + * @return + * The domain associated with the given record, or null if the record + * has no associated domain or multiple domains. + */ + public String getDomain(KeeperRecord record) { + + KeeperRecordData data = record.getData(); + List custom = data.getCustom(); + + // First check text "domain" custom field ... + Text textField = getField(custom, Text.class, DOMAIN_LABEL_PATTERN); + if (textField != null) + return getSingleStringValue(textField.getValue()); + + // ... or hidden "domain" custom field if that's not found + HiddenField hiddenField = getField(custom, HiddenField.class, DOMAIN_LABEL_PATTERN); + if (hiddenField != null) + return getSingleStringValue(hiddenField.getValue()); + + return null; + + } + /** * Returns the password associated with the given record. Both standard and * custom fields are searched. Only "Password" and "Hidden" field types are @@ -555,7 +595,7 @@ public class KsmRecordService { // a pair of custom hidden fields for the private key and passphrase: // the standard password field of the "Login" record refers to the // user's own password, if any, not the passphrase of their key) - + // Use password "private key" custom field as fallback ... Password passwordField = getField(custom, Password.class, PASSPHRASE_LABEL_PATTERN); if (passwordField != null) @@ -567,7 +607,7 @@ public class KsmRecordService { return getSingleStringValue(hiddenField.getValue()); return null; - + } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 824f9e54e..21d7c91ac 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -32,7 +32,9 @@ import java.util.concurrent.Future; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.protocol.GuacamoleConfiguration; import org.apache.guacamole.token.TokenFilter; +import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.secret.VaultSecretService; +import org.apache.guacamole.vault.secret.WindowsUsername; /** * Service which retrieves secrets from Keeper Secrets Manager. @@ -52,6 +54,12 @@ public class KsmSecretService implements VaultSecretService { @Inject private KsmRecordService recordService; + /** + * Service for retrieving configuration information. + */ + @Inject + private KsmConfigurationService confService; + @Override public String canonicalize(String nameComponent) { try { @@ -86,17 +94,48 @@ public class KsmSecretService implements VaultSecretService { * @param record * The record to retrieve secrets from when generating tokens. This may * be null. + * + * @throws GuacamoleException + * If configuration details in guacamole.properties cannot be parsed. */ private void addRecordTokens(Map> tokens, String prefix, - KeeperRecord record) { + KeeperRecord record) throws GuacamoleException { if (record == null) return; + // Domain of server-related record + String domain = recordService.getDomain(record); + if (domain != null) + tokens.put(prefix + "DOMAIN", CompletableFuture.completedFuture(domain)); + // Username of server-related record String username = recordService.getUsername(record); - if (username != null) - tokens.put(prefix + "USERNAME", CompletableFuture.completedFuture(username)); + if (username != null) { + + // If the record had no directly defined domain, but there is a + // username, and the configuration is enabled to split Windows + // domains out of usernames, attempt to split the domain out now + if (domain == null && confService.getSplitWindowsUsernames()) { + WindowsUsername usernameAndDomain = + WindowsUsername.splitWindowsUsernameFromDomain(username); + + // Always store the username token + tokens.put(prefix + "USERNAME", CompletableFuture.completedFuture( + usernameAndDomain.getUsername())); + + // Only store the domain if one is detected + if (usernameAndDomain.hasDomain()) + tokens.put(prefix + "DOMAIN", CompletableFuture.completedFuture( + usernameAndDomain.getDomain())); + + } + + // If splitting is not enabled, store the whole value in the USERNAME token + else { + tokens.put(prefix + "USERNAME", CompletableFuture.completedFuture(username)); + } + } // Password of server-related record String password = recordService.getPassword(record); @@ -113,7 +152,7 @@ public class KsmSecretService implements VaultSecretService { tokens.put(prefix + "KEY", privateKey); } - + @Override public Map> getTokens(GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException { @@ -135,7 +174,7 @@ public class KsmSecretService implements VaultSecretService { // Tokens specific to RDP if ("rdp".equals(config.getProtocol())) { - + // Retrieve and define gateway server-specific tokens, if any String gatewayHostname = parameters.get("gateway-hostname"); if (gatewayHostname != null && !gatewayHostname.isEmpty()) From f64bd30e9de561e01fcb23ba11e9444c0076f46f Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 23 Jun 2022 02:18:21 +0000 Subject: [PATCH 025/289] GUACAMOLE-1623: Use Java 8 List initializer to fix Java 8 builds. --- .../org/apache/guacamole/vault/secret/WindowsUsernameTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java index 9a4c5c716..61f454683 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/test/java/org/apache/guacamole/vault/secret/WindowsUsernameTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.Arrays; import java.util.List; /** @@ -60,7 +61,7 @@ public class WindowsUsernameTest { assertEquals("localhost", usernameAndDomain.getDomain()); // It should not match if there are an invalid number of separators - List invalidSeparators = List.of( + List invalidSeparators = Arrays.asList( "bob@local@host", "local\\host\\bob", "bob\\local@host", "local@host\\bob"); invalidSeparators.stream().forEach( From f7d90a641ee4d0d715c4b0f53f7ed1368cb850cc Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 28 Jun 2022 20:55:19 +0000 Subject: [PATCH 026/289] GUACAMOLE-1629: Add configuration properties and associated translations. --- .../vault/conf/VaultAttributeService.java | 40 ++++++++++++ .../vault/user/VaultUserContext.java | 16 +++++ .../ksm/KsmAuthenticationProviderModule.java | 3 + .../vault/ksm/conf/KsmAttributeService.java | 63 +++++++++++++++++++ .../src/main/resources/translations/en.json | 12 ++++ 5 files changed, 134 insertions(+) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java new file mode 100644 index 000000000..b77601621 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java @@ -0,0 +1,40 @@ +/* + * 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.vault.conf; + +import java.util.Collection; + +import org.apache.guacamole.form.Form; + +/** + * A service that exposes attributes for the admin UI, specific to the vault + * implementation. Any vault implementation will need to expose the attributes + * necessary for that implementation. + */ +public interface VaultAttributeService { + + /** + * Return all connection group attributes to be exposed through the admin UI. + * + * @return + * All connection group attributes to be exposed through the admin UI. + */ + public Collection getConnectionGroupAttributes(); +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index 53901483e..58056842b 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java @@ -22,12 +22,15 @@ package org.apache.guacamole.vault.user; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; + +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.form.Form; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.TokenInjectingUserContext; @@ -35,6 +38,7 @@ import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.protocol.GuacamoleConfiguration; import org.apache.guacamole.token.GuacamoleTokenUndefinedException; import org.apache.guacamole.token.TokenFilter; +import org.apache.guacamole.vault.conf.VaultAttributeService; import org.apache.guacamole.vault.conf.VaultConfigurationService; import org.apache.guacamole.vault.secret.VaultSecretService; import org.slf4j.Logger; @@ -121,6 +125,13 @@ public class VaultUserContext extends TokenInjectingUserContext { @Inject private VaultSecretService secretService; + /** + * Service for retrieving any custom attributes defined for the + * current vault implementation. + */ + @Inject + private VaultAttributeService attributeService; + /** * Creates a new VaultUserContext which automatically injects tokens * containing values of secrets retrieved from a vault. The given @@ -403,4 +414,9 @@ public class VaultUserContext extends TokenInjectingUserContext { } + @Override + public Collection getConnectionGroupAttributes() { + return attributeService.getConnectionGroupAttributes(); + } + } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java index bcc5a784e..17580b8a0 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java @@ -21,8 +21,10 @@ package org.apache.guacamole.vault.ksm; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.vault.VaultAuthenticationProviderModule; +import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmSecretService; +import org.apache.guacamole.vault.conf.VaultAttributeService; import org.apache.guacamole.vault.conf.VaultConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmClient; import org.apache.guacamole.vault.ksm.secret.KsmRecordService; @@ -51,6 +53,7 @@ public class KsmAuthenticationProviderModule // Bind services specific to Keeper Secrets Manager bind(KsmClient.class); bind(KsmRecordService.class); + bind(VaultAttributeService.class).to(KsmAttributeService.class); bind(VaultConfigurationService.class).to(KsmConfigurationService.class); bind(VaultSecretService.class).to(KsmSecretService.class); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java new file mode 100644 index 000000000..ade0f97e9 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java @@ -0,0 +1,63 @@ +/* + * 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.vault.ksm.conf; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.apache.guacamole.form.Form; +import org.apache.guacamole.form.MultilineField; +import org.apache.guacamole.vault.conf.VaultAttributeService; + +import com.google.inject.Singleton; + +/** + * A service that exposes KSM-specific attributes, allowing setting KSM + * configuration through the admin interface. + */ +@Singleton +public class KsmAttributeService implements VaultAttributeService { + + /** + * The name of the attribute which can contain a KSM configuration blob + * associated with a connection group. + */ + public static final String KSM_CONFIGURATION_ATTRIBUTE = "ksm-config"; + + /** + * All attributes related to configuring the KSM vault on a + * per-connection-group basis. + */ + public static final Form KSM_CONFIGURATION_FORM = new Form("ksm-config", + Arrays.asList(new MultilineField(KSM_CONFIGURATION_ATTRIBUTE))); + + /** + * All KSM-specific connection group attributes, organized by form. + */ + public static final Collection KSM_CONNECTION_GROUP_ATTRIBUTES = + Collections.unmodifiableCollection(Arrays.asList(KSM_CONFIGURATION_FORM)); + + @Override + public Collection getConnectionGroupAttributes() { + return KSM_CONNECTION_GROUP_ATTRIBUTES; + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json new file mode 100644 index 000000000..abda16cef --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json @@ -0,0 +1,12 @@ +{ + + "DATA_SOURCE_KEEPER_SECRETS_MANAGER" : { + "NAME" : "Keeper Secrets Manager" + }, + + "CONNECTION_GROUP_ATTRIBUTES" : { + "SECTION_HEADER_KSM_CONFIG" : "Keeper Secrets Manager", + "FIELD_HEADER_KSM_CONFIG" : "KSM Service Configuration " + } + +} From 16efc0cdc1836617933c701505ed53b877f1a121 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 28 Jun 2022 23:19:15 +0000 Subject: [PATCH 027/289] GUACAMOLE-1629: Implement multiple-vault support for KSM codebase. --- .../ksm/KsmAuthenticationProviderModule.java | 9 + .../guacamole/vault/ksm/conf/KsmConfig.java | 60 +++ .../vault/ksm/conf/KsmConfigProperty.java | 13 +- .../ksm/conf/KsmConfigurationService.java | 30 +- .../guacamole/vault/ksm/secret/KsmCache.java | 484 ++++++++++++++++++ .../vault/ksm/secret/KsmCacheFactory.java | 45 ++ .../guacamole/vault/ksm/secret/KsmClient.java | 426 ++++----------- 7 files changed, 712 insertions(+), 355 deletions(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfig.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCache.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCacheFactory.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java index 17580b8a0..9d54439ea 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java @@ -26,10 +26,14 @@ import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmSecretService; import org.apache.guacamole.vault.conf.VaultAttributeService; import org.apache.guacamole.vault.conf.VaultConfigurationService; +import org.apache.guacamole.vault.ksm.secret.KsmCache; +import org.apache.guacamole.vault.ksm.secret.KsmCacheFactory; import org.apache.guacamole.vault.ksm.secret.KsmClient; import org.apache.guacamole.vault.ksm.secret.KsmRecordService; import org.apache.guacamole.vault.secret.VaultSecretService; +import com.google.inject.assistedinject.FactoryModuleBuilder; + /** * Guice module which configures injections specific to Keeper Secrets * Manager support. @@ -56,6 +60,11 @@ public class KsmAuthenticationProviderModule bind(VaultAttributeService.class).to(KsmAttributeService.class); bind(VaultConfigurationService.class).to(KsmConfigurationService.class); bind(VaultSecretService.class).to(KsmSecretService.class); + + // Bind factory for creating KSM Caches + install(new FactoryModuleBuilder() + .implement(KsmCache.class, KsmCache.class) + .build(KsmCacheFactory.class)); } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfig.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfig.java new file mode 100644 index 000000000..54aaec753 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfig.java @@ -0,0 +1,60 @@ +/* + * 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.vault.ksm.conf; + +import com.keepersecurity.secretsManager.core.InMemoryStorage; +import com.keepersecurity.secretsManager.core.KeyValueStorage; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; + +/** + * A utility for parsing base64-encoded JSON, as output by the Keeper Commander + * CLI tool via the "sm client add" command into a Keeper Secrets Manager + * {@link KeyValueStorage} object. + */ +public class KsmConfig { + + /** + * Given a base64-encoded JSON KSM configuration, parse and return a + * KeyValueStorage object. + * + * @param value + * The base64-encoded JSON KSM configuration to parse. + * + * @return + * The KeyValueStorage that is a result of the parsing operation + * + * @throws GuacamoleException + * If the provided value is not valid base-64 encoded JSON KSM configuration. + */ + public static KeyValueStorage parseKsmConfig(String value) throws GuacamoleException { + + // Parse base64 value as KSM config storage + try { + return new InMemoryStorage(value); + } + catch (IllegalArgumentException e) { + throw new GuacamoleServerException("Invalid base64 configuration " + + "for Keeper Secrets Manager.", e); + } + + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java index aaddb0de0..afd8c921a 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java @@ -19,10 +19,8 @@ package org.apache.guacamole.vault.ksm.conf; -import com.keepersecurity.secretsManager.core.InMemoryStorage; import com.keepersecurity.secretsManager.core.KeyValueStorage; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.properties.GuacamoleProperty; /** @@ -39,15 +37,8 @@ public abstract class KsmConfigProperty implements GuacamoleProperty cachedRecordsByUid = new HashMap<>(); + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * hostname or IP address of the corresponding record. The hostname or IP + * address of a record is determined by {@link Hosts} fields, thus a record + * may be associated with multiple hosts. If a record is associated with + * multiple hosts, there will be multiple references to that record within + * this Map. The contents of this Map are automatically updated if + * {@link #validateCache()} refreshes the cache. This Map must not be + * accessed without {@link #cacheLock} acquired appropriately. Before using + * a value from this Map, {@link #cachedAmbiguousHosts} must first be + * checked to verify that there is indeed only one record associated with + * that host. + */ + private final Map cachedRecordsByHost = new HashMap<>(); + + /** + * The set of all hostnames or IP addresses that are associated with + * multiple records, and thus cannot uniquely identify a record. The + * contents of this Set are automatically updated if + * {@link #validateCache()} refreshes the cache. This Set must not be + * accessed without {@link #cacheLock} acquired appropriately.This Set + * must be checked before using a value retrieved from + * {@link #cachedRecordsByHost}. + */ + private final Set cachedAmbiguousHosts = new HashSet<>(); + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * username of the corresponding record. The username of a record is + * determined by {@link Login} fields, thus a record may be associated with + * multiple users. If a record is associated with multiple users, there + * will be multiple references to that record within this Map. The contents + * of this Map are automatically updated if {@link #validateCache()} + * refreshes the cache. This Map must not be accessed without + * {@link #cacheLock} acquired appropriately. Before using a value from + * this Map, {@link #cachedAmbiguousUsernames} must first be checked to + * verify that there is indeed only one record associated with that user. + */ + private final Map cachedRecordsByUsername = new HashMap<>(); + + /** + * The set of all usernames that are associated with multiple records, and + * thus cannot uniquely identify a record. The contents of this Set are + * automatically updated if {@link #validateCache()} refreshes the cache. + * This Set must not be accessed without {@link #cacheLock} acquired + * appropriately.This Set must be checked before using a value retrieved + * from {@link #cachedRecordsByUsername}. + */ + private final Set cachedAmbiguousUsernames = new HashSet<>(); + + /** + * Validates that all cached data is current with respect to + * {@link #CACHE_INTERVAL}, refreshing data from the server as needed. + * + * @throws GuacamoleException + * If an error occurs preventing the cached data from being refreshed. + */ + private void validateCache() throws GuacamoleException { + + long currentTime = System.currentTimeMillis(); + + // Perform a read-only check that the cache has actually expired before + // continuing + cacheLock.readLock().lock(); + try { + if (currentTime - cacheTimestamp < CACHE_INTERVAL) + return; + } + finally { + cacheLock.readLock().unlock(); + } + + cacheLock.writeLock().lock(); + try { + + // Cache may have been updated since the read-only check. Re-verify + // that the cache has expired before continuing with a full refresh + if (currentTime - cacheTimestamp < CACHE_INTERVAL) + return; + + // Attempt to pull all records first, allowing that operation to + // succeed/fail BEFORE we clear out the last cached success + KeeperSecrets secrets = SecretsManager.getSecrets(ksmConfig); + List records = secrets.getRecords(); + + // Store all secrets within cache + cachedSecrets = secrets; + + // Clear unambiguous cache of all records by UID + cachedRecordsByUid.clear(); + + // Clear cache of host-based records + cachedAmbiguousHosts.clear(); + cachedRecordsByHost.clear(); + + // Clear cache of login-based records + cachedAmbiguousUsernames.clear(); + cachedRecordsByUsername.clear(); + + // Store all records, sorting each into host-based and login-based + // buckets + records.forEach(record -> { + + // Store based on UID ... + cachedRecordsByUid.put(record.getRecordUid(), record); + + // ... and hostname/address + String hostname = recordService.getHostname(record); + addRecordForHost(record, hostname); + + // Store based on username ONLY if no hostname (will otherwise + // result in ambiguous entries for servers tied to identical + // accounts) + if (hostname == null) + addRecordForLogin(record, recordService.getUsername(record)); + + }); + + // Cache has been refreshed + this.cacheTimestamp = System.currentTimeMillis(); + + } + finally { + cacheLock.writeLock().unlock(); + } + + } + + /** + * Associates the given record with the given hostname. The hostname may be + * null. Both {@link #cachedRecordsByHost} and {@link #cachedAmbiguousHosts} + * are updated appropriately. The write lock of {@link #cacheLock} must + * already be acquired before invoking this function. + * + * @param record + * The record to associate with the hosts in the given field. + * + * @param hostname + * The hostname/address that the given record should be associated + * with. This may be null. + */ + private void addRecordForHost(KeeperRecord record, String hostname) { + + if (hostname == null) + return; + + KeeperRecord existing = cachedRecordsByHost.putIfAbsent(hostname, record); + if (existing != null && record != existing) + cachedAmbiguousHosts.add(hostname); + + } + + /** + * Associates the given record with the given username. The given username + * may be null. Both {@link #cachedRecordsByUsername} and + * {@link #cachedAmbiguousUsernames} are updated appropriately. The write + * lock of {@link #cacheLock} must already be acquired before invoking this + * function. + * + * @param record + * The record to associate with the given username. + * + * @param username + * The username that the given record should be associated with. This + * may be null. + */ + private void addRecordForLogin(KeeperRecord record, String username) { + + if (username == null) + return; + + KeeperRecord existing = cachedRecordsByUsername.putIfAbsent(username, record); + if (existing != null && record != existing) + cachedAmbiguousUsernames.add(username); + + } + + /** + * Returns all records accessible via Keeper Secrets Manager. The records + * returned are arbitrarily ordered. + * + * @return + * An unmodifiable Collection of all records accessible via Keeper + * Secrets Manager, in no particular order. + * + * @throws GuacamoleException + * If an error occurs that prevents records from being retrieved. + */ + public Collection getRecords() throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + return Collections.unmodifiableCollection(cachedRecordsByUid.values()); + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the record having the given UID. If no such record exists, null + * is returned. + * + * @param uid + * The UID of the record to return. + * + * @return + * The record having the given UID, or null if there is no such record. + * + * @throws GuacamoleException + * If an error occurs that prevents the record from being retrieved. + */ + public KeeperRecord getRecord(String uid) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + return cachedRecordsByUid.get(uid); + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the record associated with the given hostname or IP address. If + * no such record exists, or there are multiple such records, null is + * returned. + * + * @param hostname + * The hostname of the record to return. + * + * @return + * The record associated with the given hostname, or null if there is + * no such record or multiple such records. + * + * @throws GuacamoleException + * If an error occurs that prevents the record from being retrieved. + */ + public KeeperRecord getRecordByHost(String hostname) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + + if (cachedAmbiguousHosts.contains(hostname)) { + logger.debug("The hostname/address \"{}\" is referenced by " + + "multiple Keeper records and cannot be used to " + + "locate individual secrets.", hostname); + return null; + } + + return cachedRecordsByHost.get(hostname); + + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the record associated with the given username. If no such record + * exists, or there are multiple such records, null is returned. + * + * @param username + * The username of the record to return. + * + * @return + * The record associated with the given username, or null if there is + * no such record or multiple such records. + * + * @throws GuacamoleException + * If an error occurs that prevents the record from being retrieved. + */ + public KeeperRecord getRecordByLogin(String username) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + + if (cachedAmbiguousUsernames.contains(username)) { + logger.debug("The username \"{}\" is referenced by multiple " + + "Keeper records and cannot be used to locate " + + "individual secrets.", username); + return null; + } + + return cachedRecordsByUsername.get(username); + + } + finally { + cacheLock.readLock().unlock(); + } + } + + /** + * Returns the value of the secret stored within Keeper Secrets Manager and + * represented by the given Keeper notation. Keeper notation locates the + * value of a specific field, custom field, or file associated with a + * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation + * + * @param notation + * The Keeper notation of the secret to retrieve. + * + * @return + * A Future which completes with the value of the secret represented by + * the given Keeper notation, or null if there is no such secret. + * + * @throws GuacamoleException + * If the requested secret cannot be retrieved or the Keeper notation + * is invalid. + */ + public Future getSecret(String notation) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + + // Retrieve any relevant file asynchronously + Matcher fileNotationMatcher = KEEPER_FILE_NOTATION.matcher(notation); + if (fileNotationMatcher.matches()) + return recordService.download(Notation.getFile(cachedSecrets, notation)); + + // Retrieve string values synchronously + return CompletableFuture.completedFuture(Notation.getValue(cachedSecrets, notation)); + + } + + // Unfortunately, the notation parser within the Keeper SDK throws + // plain Errors for retrieval failures ... + catch (Error e) { + logger.warn("Record \"{}\" does not exist.", notation); + logger.debug("Retrieval of record by Keeper notation failed.", e); + return CompletableFuture.completedFuture(null); + } + + // ... and plain Exceptions for parse failures (no subclasses) + catch (Exception e) { + logger.warn("\"{}\" is not valid Keeper notation. Please check " + + "the documentation at {} for valid formatting.", + notation, KEEPER_NOTATION_DOC_URL); + logger.debug("Provided Keeper notation could not be parsed.", e); + return CompletableFuture.completedFuture(null); + } + finally { + cacheLock.readLock().unlock(); + } + + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCacheFactory.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCacheFactory.java new file mode 100644 index 000000000..4da9ee6bd --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCacheFactory.java @@ -0,0 +1,45 @@ +/* + * 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.vault.ksm.secret; + +import javax.annotation.Nonnull; + +import com.keepersecurity.secretsManager.core.SecretsManagerOptions; + +/** + * Factory for creating KsmCache instances. + */ +public interface KsmCacheFactory { + + /** + * Returns a new instance of a KsmCache instance associated with + * the provided KSM configuration options. + * + * @param ksmConfigOptions + * The KSM config options to use when constructing the KsmCache + * object. + * + * @return + * A new KsmCache instance associated with the provided KSM config + * options. + */ + KsmCache create(@Nonnull SecretsManagerOptions ksmConfigOptions); + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index fa177014b..ae80e68e2 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -21,29 +21,18 @@ package org.apache.guacamole.vault.ksm.secret; import com.google.inject.Inject; import com.google.inject.Singleton; -import com.keepersecurity.secretsManager.core.Hosts; import com.keepersecurity.secretsManager.core.KeeperRecord; -import com.keepersecurity.secretsManager.core.KeeperSecrets; -import com.keepersecurity.secretsManager.core.Login; -import com.keepersecurity.secretsManager.core.Notation; -import com.keepersecurity.secretsManager.core.SecretsManager; +import com.keepersecurity.secretsManager.core.SecretsManagerOptions; + import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + +import javax.annotation.Nullable; + import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Client which retrieves records from Keeper Secrets Manager, allowing @@ -56,11 +45,6 @@ import org.slf4j.LoggerFactory; @Singleton public class KsmClient { - /** - * Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger(KsmClient.class); - /** * Service for retrieving configuration information. */ @@ -68,268 +52,78 @@ public class KsmClient { private KsmConfigurationService confService; /** - * Service for retrieving data from records. + * Factory for creating KSM cache instances for particular KSM configs. */ @Inject - private KsmRecordService recordService; + private KsmCacheFactory ksmCacheFactory; /** - * The publicly-accessible URL for Keeper's documentation covering Keeper - * notation. + * A map of base-64 encoded JSON KSM config blobs to associated KSM cache instances. + * The `null` entry in this Map is associated with the KSM configuration parsed + * from the guacamole.properties config file. */ - private static final String KEEPER_NOTATION_DOC_URL = - "https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation"; + private final Map ksmCacheMap = new HashMap<>(); /** - * The regular expression that Keeper notation must match to be related to - * file retrieval. As the Keeper SDK provides mutually-exclusive for - * retrieving secret values and files via notation, the notation must first - * be tested to determine whether it refers to a file. - */ - private static final Pattern KEEPER_FILE_NOTATION = Pattern.compile("^(keeper://)?[^/]*/file/.+"); - - /** - * The maximum amount of time that an entry will be stored in the cache - * before being refreshed, in milliseconds. - */ - private static final long CACHE_INTERVAL = 5000; - - /** - * Read/write lock which guards access to all cached data, including the - * timestamp recording the last time the cache was refreshed. Readers of - * the cache must first acquire (and eventually release) the read lock, and - * writers of the cache must first acquire (and eventually release) the - * write lock. - */ - private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); - - /** - * The timestamp that the cache was last refreshed, in milliseconds, as - * returned by System.currentTimeMillis(). This value is automatically - * updated if {@link #validateCache()} refreshes the cache. This value must - * not be accessed without {@link #cacheLock} acquired appropriately. - */ - private volatile long cacheTimestamp = 0; - - /** - * The full cached set of secrets last retrieved from Keeper Secrets - * Manager. This value is automatically updated if {@link #validateCache()} - * refreshes the cache. This value must not be accessed without - * {@link #cacheLock} acquired appropriately. - */ - private KeeperSecrets cachedSecrets = null; - - /** - * All records retrieved from Keeper Secrets Manager, where each key is the - * UID of the corresponding record. The contents of this Map are - * automatically updated if {@link #validateCache()} refreshes the cache. - * This Map must not be accessed without {@link #cacheLock} acquired - * appropriately. - */ - private final Map cachedRecordsByUid = new HashMap<>(); - - /** - * All records retrieved from Keeper Secrets Manager, where each key is the - * hostname or IP address of the corresponding record. The hostname or IP - * address of a record is determined by {@link Hosts} fields, thus a record - * may be associated with multiple hosts. If a record is associated with - * multiple hosts, there will be multiple references to that record within - * this Map. The contents of this Map are automatically updated if - * {@link #validateCache()} refreshes the cache. This Map must not be - * accessed without {@link #cacheLock} acquired appropriately. Before using - * a value from this Map, {@link #cachedAmbiguousHosts} must first be - * checked to verify that there is indeed only one record associated with - * that host. - */ - private final Map cachedRecordsByHost = new HashMap<>(); - - /** - * The set of all hostnames or IP addresses that are associated with - * multiple records, and thus cannot uniquely identify a record. The - * contents of this Set are automatically updated if - * {@link #validateCache()} refreshes the cache. This Set must not be - * accessed without {@link #cacheLock} acquired appropriately.This Set - * must be checked before using a value retrieved from - * {@link #cachedRecordsByHost}. - */ - private final Set cachedAmbiguousHosts = new HashSet<>(); - - /** - * All records retrieved from Keeper Secrets Manager, where each key is the - * username of the corresponding record. The username of a record is - * determined by {@link Login} fields, thus a record may be associated with - * multiple users. If a record is associated with multiple users, there - * will be multiple references to that record within this Map. The contents - * of this Map are automatically updated if {@link #validateCache()} - * refreshes the cache. This Map must not be accessed without - * {@link #cacheLock} acquired appropriately. Before using a value from - * this Map, {@link #cachedAmbiguousUsernames} must first be checked to - * verify that there is indeed only one record associated with that user. - */ - private final Map cachedRecordsByUsername = new HashMap<>(); - - /** - * The set of all usernames that are associated with multiple records, and - * thus cannot uniquely identify a record. The contents of this Set are - * automatically updated if {@link #validateCache()} refreshes the cache. - * This Set must not be accessed without {@link #cacheLock} acquired - * appropriately.This Set must be checked before using a value retrieved - * from {@link #cachedRecordsByUsername}. - */ - private final Set cachedAmbiguousUsernames = new HashSet<>(); - - /** - * Validates that all cached data is current with respect to - * {@link #CACHE_INTERVAL}, refreshing data from the server as needed. + * Create and return a KSM cache for the provided KSM config if not already + * present in the cache map, the existing cache entry. + * + * @param ksmConfig + * The base-64 encoded JSON KSM config blob associated with the cache entry. + * If an associated entry does not already exist, it will be created using + * this configuration. + * + * @return + * A KSM cache for the provided KSM config if not already present in the + * cache map, otherwise the existing cache entry. * * @throws GuacamoleException - * If an error occurs preventing the cached data from being refreshed. + * If an error occurs while creating the KSM cache. */ - private void validateCache() throws GuacamoleException { + private KsmCache createCacheIfNeeded(@Nullable String ksmConfig) + throws GuacamoleException { - long currentTime = System.currentTimeMillis(); - - // Perform a read-only check that the cache has actually expired before - // continuing - cacheLock.readLock().lock(); - try { - if (currentTime - cacheTimestamp < CACHE_INTERVAL) - return; - } - finally { - cacheLock.readLock().unlock(); - } - - cacheLock.writeLock().lock(); - try { - - // Cache may have been updated since the read-only check. Re-verify - // that the cache has expired before continuing with a full refresh - if (currentTime - cacheTimestamp < CACHE_INTERVAL) - return; - - // Attempt to pull all records first, allowing that operation to - // succeed/fail BEFORE we clear out the last cached success - KeeperSecrets secrets = SecretsManager.getSecrets(confService.getSecretsManagerOptions()); - List records = secrets.getRecords(); - - // Store all secrets within cache - cachedSecrets = secrets; - - // Clear unambiguous cache of all records by UID - cachedRecordsByUid.clear(); - - // Clear cache of host-based records - cachedAmbiguousHosts.clear(); - cachedRecordsByHost.clear(); - - // Clear cache of login-based records - cachedAmbiguousUsernames.clear(); - cachedRecordsByUsername.clear(); - - // Store all records, sorting each into host-based and login-based - // buckets - records.forEach(record -> { - - // Store based on UID ... - cachedRecordsByUid.put(record.getRecordUid(), record); - - // ... and hostname/address - String hostname = recordService.getHostname(record); - addRecordForHost(record, hostname); - - // Store based on username ONLY if no hostname (will otherwise - // result in ambiguous entries for servers tied to identical - // accounts) - if (hostname == null) - addRecordForLogin(record, recordService.getUsername(record)); - - }); - - // Cache has been refreshed - this.cacheTimestamp = System.currentTimeMillis(); - - } - finally { - cacheLock.writeLock().unlock(); - } + // If a cache already exists for the provided config, use it + KsmCache ksmCache = ksmCacheMap.get(ksmConfig); + if (ksmCache != null) + return ksmCache; + // Create and store a new KSM cache instance for the provided KSM config blob + SecretsManagerOptions options = confService.getSecretsManagerOptions(ksmConfig); + return ksmCacheMap.put(ksmConfig, ksmCacheFactory.create(options)); } /** - * Associates the given record with the given hostname. The hostname may be - * null. Both {@link #cachedRecordsByHost} and {@link #cachedAmbiguousHosts} - * are updated appropriately. The write lock of {@link #cacheLock} must - * already be acquired before invoking this function. + * Returns all records accessible via Keeper Secrets Manager, associated + * with the provided KSM config. If no KSM config is provided, records + * associated with the default configuration as retrieved from the config + * file will be used. The records returned are arbitrarily ordered. * - * @param record - * The record to associate with the hosts in the given field. - * - * @param hostname - * The hostname/address that the given record should be associated - * with. This may be null. - */ - private void addRecordForHost(KeeperRecord record, String hostname) { - - if (hostname == null) - return; - - KeeperRecord existing = cachedRecordsByHost.putIfAbsent(hostname, record); - if (existing != null && record != existing) - cachedAmbiguousHosts.add(hostname); - - } - - /** - * Associates the given record with the given username. The given username - * may be null. Both {@link #cachedRecordsByUsername} and - * {@link #cachedAmbiguousUsernames} are updated appropriately. The write - * lock of {@link #cacheLock} must already be acquired before invoking this - * function. - * - * @param record - * The record to associate with the given username. - * - * @param username - * The username that the given record should be associated with. This - * may be null. - */ - private void addRecordForLogin(KeeperRecord record, String username) { - - if (username == null) - return; - - KeeperRecord existing = cachedRecordsByUsername.putIfAbsent(username, record); - if (existing != null && record != existing) - cachedAmbiguousUsernames.add(username); - - } - - /** - * Returns all records accessible via Keeper Secrets Manager. The records - * returned are arbitrarily ordered. + * @param ksmConfig + * The base-64 encoded JSON KSM config blob associated associated with + * the KSM vault that should be used to fetch the records. * * @return * An unmodifiable Collection of all records accessible via Keeper - * Secrets Manager, in no particular order. + * Secrets Manager, in no particular order. * * @throws GuacamoleException * If an error occurs that prevents records from being retrieved. */ - public Collection getRecords() throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { - return Collections.unmodifiableCollection(cachedRecordsByUid.values()); - } - finally { - cacheLock.readLock().unlock(); - } + public Collection getRecords( + @Nullable String ksmConfig) throws GuacamoleException { + + // Call through to the associated KSM cache instance + return createCacheIfNeeded(ksmConfig).getRecords(); } /** - * Returns the record having the given UID. If no such record exists, null - * is returned. + * Returns the record having the given KSM config, and the given UID. + * If no such record exists, null is returned. + * + * @param ksmConfig + * The base-64 encoded JSON KSM config blob associated associated with + * the KSM vault that should be used to fetch the record. * * @param uid * The UID of the record to return. @@ -340,21 +134,21 @@ public class KsmClient { * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ - public KeeperRecord getRecord(String uid) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { - return cachedRecordsByUid.get(uid); - } - finally { - cacheLock.readLock().unlock(); - } + public KeeperRecord getRecord( + @Nullable String ksmConfig, String uid) throws GuacamoleException { + + // Call through to the associated KSM cache instance + return createCacheIfNeeded(ksmConfig).getRecord(uid); } /** - * Returns the record associated with the given hostname or IP address. If - * no such record exists, or there are multiple such records, null is - * returned. + * Returns the record associated with the given hostname or IP address + * and the given KSM config. If no such record exists, or there are multiple + * such records, null is returned. + * + * @param ksmConfig + * The base-64 encoded JSON KSM config blob associated associated with + * the KSM vault that should be used to fetch the record. * * @param hostname * The hostname of the record to return. @@ -366,30 +160,22 @@ public class KsmClient { * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ - public KeeperRecord getRecordByHost(String hostname) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { + public KeeperRecord getRecordByHost( + @Nullable String ksmConfig, String hostname) throws GuacamoleException { - if (cachedAmbiguousHosts.contains(hostname)) { - logger.debug("The hostname/address \"{}\" is referenced by " - + "multiple Keeper records and cannot be used to " - + "locate individual secrets.", hostname); - return null; - } + // Call through to the associated KSM cache instance + return createCacheIfNeeded(ksmConfig).getRecordByHost(hostname); - return cachedRecordsByHost.get(hostname); - - } - finally { - cacheLock.readLock().unlock(); - } } /** * Returns the record associated with the given username. If no such record * exists, or there are multiple such records, null is returned. * + * @param ksmConfig + * The base-64 encoded JSON KSM config blob associated associated with + * the KSM vault that should be used to fetch the record. + * * @param username * The username of the record to return. * @@ -400,31 +186,24 @@ public class KsmClient { * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ - public KeeperRecord getRecordByLogin(String username) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { + public KeeperRecord getRecordByLogin( + @Nullable String ksmConfig, String username) throws GuacamoleException { - if (cachedAmbiguousUsernames.contains(username)) { - logger.debug("The username \"{}\" is referenced by multiple " - + "Keeper records and cannot be used to locate " - + "individual secrets.", username); - return null; - } + // Call through to the associated KSM cache instance + return createCacheIfNeeded(ksmConfig).getRecordByLogin(username); - return cachedRecordsByUsername.get(username); - - } - finally { - cacheLock.readLock().unlock(); - } } /** - * Returns the value of the secret stored within Keeper Secrets Manager and - * represented by the given Keeper notation. Keeper notation locates the - * value of a specific field, custom field, or file associated with a - * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation + * Returns the value of the secret stored within the Keeper Secrets Manager vault + * associated with the provided KSM config, and represented by the given Keeper + * notation. Keeper notation locates the value of a specific field, custom field, + * or file associated with a specific record. + * See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation + * + * @param ksmConfig + * The base-64 encoded JSON KSM config blob associated associated with + * the KSM vault that should be used to fetch the record. * * @param notation * The Keeper notation of the secret to retrieve. @@ -437,40 +216,11 @@ public class KsmClient { * If the requested secret cannot be retrieved or the Keeper notation * is invalid. */ - public Future getSecret(String notation) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { + public Future getSecret( + @Nullable String ksmConfig, String notation) throws GuacamoleException { - // Retrieve any relevant file asynchronously - Matcher fileNotationMatcher = KEEPER_FILE_NOTATION.matcher(notation); - if (fileNotationMatcher.matches()) - return recordService.download(Notation.getFile(cachedSecrets, notation)); - - // Retrieve string values synchronously - return CompletableFuture.completedFuture(Notation.getValue(cachedSecrets, notation)); - - } - - // Unfortunately, the notation parser within the Keeper SDK throws - // plain Errors for retrieval failures ... - catch (Error e) { - logger.warn("Record \"{}\" does not exist.", notation); - logger.debug("Retrieval of record by Keeper notation failed.", e); - return CompletableFuture.completedFuture(null); - } - - // ... and plain Exceptions for parse failures (no subclasses) - catch (Exception e) { - logger.warn("\"{}\" is not valid Keeper notation. Please check " - + "the documentation at {} for valid formatting.", - notation, KEEPER_NOTATION_DOC_URL); - logger.debug("Provided Keeper notation could not be parsed.", e); - return CompletableFuture.completedFuture(null); - } - finally { - cacheLock.readLock().unlock(); - } + // Call through to the associated KSM cache instance + return createCacheIfNeeded(ksmConfig).getSecret(notation); } From fee2f8b416afdaf8d574d8b9a3bb2f98addaa133 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 29 Jun 2022 20:29:58 +0000 Subject: [PATCH 028/289] GUACAMOLE-1629: Hook KSM vault code into base vault code and clean up. --- .../vault/secret/VaultSecretService.java | 45 +- .../vault/user/VaultUserContext.java | 23 +- .../ksm/KsmAuthenticationProviderModule.java | 10 +- .../vault/ksm/conf/KsmConfigProperty.java | 44 -- .../ksm/conf/KsmConfigurationService.java | 72 ++- .../guacamole/vault/ksm/secret/KsmCache.java | 484 ------------------ .../guacamole/vault/ksm/secret/KsmClient.java | 448 ++++++++++++---- ...acheFactory.java => KsmClientFactory.java} | 12 +- .../vault/ksm/secret/KsmSecretService.java | 156 +++++- 9 files changed, 627 insertions(+), 667 deletions(-) delete mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java delete mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCache.java rename extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/{KsmCacheFactory.java => KsmClientFactory.java} (79%) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java index 76349bad9..81204beb5 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/secret/VaultSecretService.java @@ -22,6 +22,8 @@ package org.apache.guacamole.vault.secret; import java.util.Map; import java.util.concurrent.Future; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.Connectable; +import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.protocol.GuacamoleConfiguration; import org.apache.guacamole.token.TokenFilter; @@ -55,7 +57,9 @@ public interface VaultSecretService { /** * Returns a Future which eventually completes with the value of the secret * having the given name. If no such secret exists, the Future will be - * completed with null. + * completed with null. The secrets retrieved from this method are independent + * of the context of the particular connection being established, or any + * associated user context. * * @param name * The name of the secret to retrieve. @@ -72,6 +76,35 @@ public interface VaultSecretService { */ Future getValue(String name) throws GuacamoleException; + /** + * Returns a Future which eventually completes with the value of the secret + * having the given name. If no such secret exists, the Future will be + * completed with null. The connection or connection group, as well as the + * user context associated with the request are provided for additional context. + * + * @param userContext + * The user context associated with the connection or connection group for + * which the secret is being retrieved. + * + * @param connectable + * The connection or connection group for which the secret is being retrieved. + * + * @param name + * The name of the secret to retrieve. + * + * @return + * A Future which completes with value of the secret having the given + * name. If no such secret exists, the Future will be completed with + * null. If an error occurs asynchronously which prevents retrieval of + * the secret, that error will be exposed through an ExecutionException + * when an attempt is made to retrieve the value from the Future. + * + * @throws GuacamoleException + * If the secret cannot be retrieved due to an error. + */ + Future getValue(UserContext userContext, Connectable connectable, + String name) throws GuacamoleException; + /** * Returns a map of token names to corresponding Futures which eventually * complete with the value of that token, where each token is dynamically @@ -80,6 +113,12 @@ public interface VaultSecretService { * function should be implemented to provide automatic tokens for those * secrets and remove the need for manual mapping via YAML. * + * @param userContext + * The user context from which the connectable originated. + * + * @param connectable + * The connection or connection group for which the tokens are being replaced. + * * @param config * The configuration of the Guacamole connection for which tokens are * being generated. This configuration may be empty or partial, @@ -99,7 +138,7 @@ public interface VaultSecretService { * If an error occurs producing the tokens and values required for the * given configuration. */ - Map> getTokens(GuacamoleConfiguration config, - TokenFilter filter) throws GuacamoleException; + Map> getTokens(UserContext userContext, Connectable connectable, + GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException; } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index 58056842b..876d7d186 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java @@ -31,6 +31,7 @@ import java.util.concurrent.Future; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.form.Form; +import org.apache.guacamole.net.auth.Connectable; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.TokenInjectingUserContext; @@ -193,6 +194,10 @@ public class VaultUserContext extends TokenInjectingUserContext { * corresponding values from the vault, using the given TokenFilter to * filter tokens within the secret names prior to retrieving those secrets. * + * @param connectable + * The connection or connection group to which the connection is being + * established. + * * @param tokenMapping * The mapping dictating the name of the secret which maps to each * parameter token, where the key is the name of the parameter token @@ -222,7 +227,8 @@ public class VaultUserContext extends TokenInjectingUserContext { * If the value for any applicable secret cannot be retrieved from the * vault due to an error. */ - private Map> getTokens(Map tokenMapping, + private Map> getTokens( + Connectable connectable, Map tokenMapping, TokenFilter secretNameFilter, GuacamoleConfiguration config, TokenFilter configFilter) throws GuacamoleException { @@ -247,14 +253,16 @@ public class VaultUserContext extends TokenInjectingUserContext { // Initiate asynchronous retrieval of the token value String tokenName = entry.getKey(); - Future secret = secretService.getValue(secretName); + Future secret = secretService.getValue( + this, connectable, secretName); pendingTokens.put(tokenName, secret); } // Additionally include any dynamic, parameter-based tokens - pendingTokens.putAll(secretService.getTokens(config, configFilter)); - + pendingTokens.putAll(secretService.getTokens( + this, connectable, config, configFilter)); + return pendingTokens; } @@ -329,7 +337,8 @@ public class VaultUserContext extends TokenInjectingUserContext { // Substitute tokens producing secret names, retrieving and storing // those secrets as parameter tokens - tokens.putAll(resolve(getTokens(confService.getTokenMapping(), filter, + tokens.putAll(resolve(getTokens( + connectionGroup, confService.getTokenMapping(), filter, null, new TokenFilter(tokens)))); } @@ -409,8 +418,8 @@ public class VaultUserContext extends TokenInjectingUserContext { // Substitute tokens producing secret names, retrieving and storing // those secrets as parameter tokens - tokens.putAll(resolve(getTokens(confService.getTokenMapping(), filter, - config, new TokenFilter(tokens)))); + tokens.putAll(resolve(getTokens(connection, confService.getTokenMapping(), + filter, config, new TokenFilter(tokens)))); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java index 9d54439ea..6a7a70c83 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java @@ -26,9 +26,8 @@ import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmSecretService; import org.apache.guacamole.vault.conf.VaultAttributeService; import org.apache.guacamole.vault.conf.VaultConfigurationService; -import org.apache.guacamole.vault.ksm.secret.KsmCache; -import org.apache.guacamole.vault.ksm.secret.KsmCacheFactory; import org.apache.guacamole.vault.ksm.secret.KsmClient; +import org.apache.guacamole.vault.ksm.secret.KsmClientFactory; import org.apache.guacamole.vault.ksm.secret.KsmRecordService; import org.apache.guacamole.vault.secret.VaultSecretService; @@ -55,16 +54,15 @@ public class KsmAuthenticationProviderModule protected void configureVault() { // Bind services specific to Keeper Secrets Manager - bind(KsmClient.class); bind(KsmRecordService.class); bind(VaultAttributeService.class).to(KsmAttributeService.class); bind(VaultConfigurationService.class).to(KsmConfigurationService.class); bind(VaultSecretService.class).to(KsmSecretService.class); - // Bind factory for creating KSM Caches + // Bind factory for creating KSM Clients install(new FactoryModuleBuilder() - .implement(KsmCache.class, KsmCache.class) - .build(KsmCacheFactory.class)); + .implement(KsmClient.class, KsmClient.class) + .build(KsmClientFactory.class)); } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java deleted file mode 100644 index afd8c921a..000000000 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigProperty.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.vault.ksm.conf; - -import com.keepersecurity.secretsManager.core.KeyValueStorage; -import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.properties.GuacamoleProperty; - -/** - * A GuacamoleProperty whose value is Keeper Secrets Manager {@link KeyValueStorage} - * object. The value of this property must be base64-encoded JSON, as output by - * the Keeper Commander CLI tool via the "sm client add" command. - */ -public abstract class KsmConfigProperty implements GuacamoleProperty { - - @Override - public KeyValueStorage parseValue(String value) throws GuacamoleException { - - // If no property provided, return null. - if (value == null) - return null; - - // Parse the base-64 encoded JSON into a KeyValueStorage object - return KsmConfig.parseKsmConfig(value); - } - -} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java index c50662cf5..3ef02b8b9 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java @@ -22,13 +22,16 @@ package org.apache.guacamole.vault.ksm.conf; import com.google.inject.Inject; import com.google.inject.Singleton; -import javax.annotation.Nullable; +import javax.annotation.Nonnull; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.BooleanGuacamoleProperty; +import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.vault.conf.VaultConfigurationService; +import com.keepersecurity.secretsManager.core.InMemoryStorage; import com.keepersecurity.secretsManager.core.KeyValueStorage; import com.keepersecurity.secretsManager.core.SecretsManagerOptions; @@ -62,7 +65,7 @@ public class KsmConfigurationService extends VaultConfigurationService { * The base64-encoded configuration information generated by the Keeper * Commander CLI tool. */ - private static final KsmConfigProperty KSM_CONFIG = new KsmConfigProperty() { + private static final StringGuacamoleProperty KSM_CONFIG = new StringGuacamoleProperty() { @Override public String getName() { @@ -127,35 +130,68 @@ public class KsmConfigurationService extends VaultConfigurationService { return environment.getProperty(STRIP_WINDOWS_DOMAINS, false); } + + /** + * Return the globally-defined base-64-encoded JSON KSM configuration blob + * as a string. + * + * @return + * The globally-defined base-64-encoded JSON KSM configuration blob + * as a string. + * + * @throws GuacamoleException + * If the value specified within guacamole.properties cannot be + * parsed or does not exist. + */ + public String getKsmConfig() throws GuacamoleException { + return environment.getRequiredProperty(KSM_CONFIG); + } + + /** + * Given a base64-encoded JSON KSM configuration, parse and return a + * KeyValueStorage object. + * + * @param value + * The base64-encoded JSON KSM configuration to parse. + * + * @return + * The KeyValueStorage that is a result of the parsing operation + * + * @throws GuacamoleException + * If the provided value is not valid base-64 encoded JSON KSM configuration. + */ + private static KeyValueStorage parseKsmConfig(String value) throws GuacamoleException { + + // Parse base64 value as KSM config storage + try { + return new InMemoryStorage(value); + } + catch (IllegalArgumentException e) { + throw new GuacamoleServerException("Invalid base64 configuration " + + "for Keeper Secrets Manager.", e); + } + + } + /** * Returns the options required to authenticate with Keeper Secrets Manager * when retrieving secrets. These options are read from the contents of * base64-encoded JSON configuration data generated by the Keeper Commander - * CLI tool. This configuration data may be passed directly as an argument. - * If not provided as an argument, it must be present in the config file. + * CLI tool. This configuration data must be passed directly as an argument. * * @param ksmConfig - * Optional KSM config data. If provided, it will be used instead of any - * KSM config data present in the config file. If not provided, the value - * in the config file will be used. + * The KSM configuration blob to parse. * * @return * The options that should be used when connecting to Keeper Secrets * Manager when retrieving secrets. * * @throws GuacamoleException - * If an invalid ksmConfig parameter is provided, or required properties - * are not specified within guacamole.properties or cannot be parsed, or - * the KSM configuration cannot be parsed. + * If an invalid ksmConfig parameter is provided. */ - public SecretsManagerOptions getSecretsManagerOptions(@Nullable String ksmConfig) throws GuacamoleException { + public SecretsManagerOptions getSecretsManagerOptions(@Nonnull String ksmConfig) throws GuacamoleException { - // Attempt to parse the KSM config provided as an argument if available. - // If not provided as an argument, it must be present in the config file. - KeyValueStorage parsedKsmConfig = ksmConfig != null - ? KsmConfig.parseKsmConfig(ksmConfig) - : environment.getRequiredProperty(KSM_CONFIG); - - return new SecretsManagerOptions(parsedKsmConfig, null, getAllowUnverifiedCertificate()); + return new SecretsManagerOptions( + parseKsmConfig(ksmConfig), null, getAllowUnverifiedCertificate()); } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCache.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCache.java deleted file mode 100644 index 178493871..000000000 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCache.java +++ /dev/null @@ -1,484 +0,0 @@ -/* - * 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.vault.ksm.secret; - -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; -import com.keepersecurity.secretsManager.core.Hosts; -import com.keepersecurity.secretsManager.core.KeeperRecord; -import com.keepersecurity.secretsManager.core.KeeperSecrets; -import com.keepersecurity.secretsManager.core.Login; -import com.keepersecurity.secretsManager.core.Notation; -import com.keepersecurity.secretsManager.core.SecretsManager; -import com.keepersecurity.secretsManager.core.SecretsManagerOptions; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.guacamole.GuacamoleException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A cache of Keeper Secrets Manager records, specific to a single KSM account. This class - * includes internal functionality necessary to fetch records from KSM as needed. - */ -public class KsmCache { - /** - * Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger(KsmCache.class); - - /** - * Service for retrieving data from records. - */ - @Inject - private KsmRecordService recordService; - - /** - * The publicly-accessible URL for Keeper's documentation covering Keeper - * notation. - */ - private static final String KEEPER_NOTATION_DOC_URL = - "https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation"; - - /** - * The regular expression that Keeper notation must match to be related to - * file retrieval. As the Keeper SDK provides mutually-exclusive for - * retrieving secret values and files via notation, the notation must first - * be tested to determine whether it refers to a file. - */ - private static final Pattern KEEPER_FILE_NOTATION = Pattern.compile("^(keeper://)?[^/]*/file/.+"); - - /** - * The maximum amount of time that an entry will be stored in the cache - * before being refreshed, in milliseconds. - */ - private static final long CACHE_INTERVAL = 5000; - - /** - * The KSM configuration associated with this cache instance. - */ - private final SecretsManagerOptions ksmConfig; - - /** - * Create a new KSM cache instance based around the provided KSM configuration. - * - * @param ksmConfig - * The KSM configuration to use when retrieving properties from KSM. - */ - @AssistedInject - public KsmCache(@Assisted SecretsManagerOptions ksmConfig) { - this.ksmConfig = ksmConfig; - } - - /** - * Read/write lock which guards access to all cached data, including the - * timestamp recording the last time the cache was refreshed. Readers of - * the cache must first acquire (and eventually release) the read lock, and - * writers of the cache must first acquire (and eventually release) the - * write lock. - */ - private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); - - /** - * The timestamp that the cache was last refreshed, in milliseconds, as - * returned by System.currentTimeMillis(). This value is automatically - * updated if {@link #validateCache()} refreshes the cache. This value must - * not be accessed without {@link #cacheLock} acquired appropriately. - */ - private volatile long cacheTimestamp = 0; - - /** - * The full cached set of secrets last retrieved from Keeper Secrets - * Manager. This value is automatically updated if {@link #validateCache()} - * refreshes the cache. This value must not be accessed without - * {@link #cacheLock} acquired appropriately. - */ - private KeeperSecrets cachedSecrets = null; - - /** - * All records retrieved from Keeper Secrets Manager, where each key is the - * UID of the corresponding record. The contents of this Map are - * automatically updated if {@link #validateCache()} refreshes the cache. - * This Map must not be accessed without {@link #cacheLock} acquired - * appropriately. - */ - private final Map cachedRecordsByUid = new HashMap<>(); - - /** - * All records retrieved from Keeper Secrets Manager, where each key is the - * hostname or IP address of the corresponding record. The hostname or IP - * address of a record is determined by {@link Hosts} fields, thus a record - * may be associated with multiple hosts. If a record is associated with - * multiple hosts, there will be multiple references to that record within - * this Map. The contents of this Map are automatically updated if - * {@link #validateCache()} refreshes the cache. This Map must not be - * accessed without {@link #cacheLock} acquired appropriately. Before using - * a value from this Map, {@link #cachedAmbiguousHosts} must first be - * checked to verify that there is indeed only one record associated with - * that host. - */ - private final Map cachedRecordsByHost = new HashMap<>(); - - /** - * The set of all hostnames or IP addresses that are associated with - * multiple records, and thus cannot uniquely identify a record. The - * contents of this Set are automatically updated if - * {@link #validateCache()} refreshes the cache. This Set must not be - * accessed without {@link #cacheLock} acquired appropriately.This Set - * must be checked before using a value retrieved from - * {@link #cachedRecordsByHost}. - */ - private final Set cachedAmbiguousHosts = new HashSet<>(); - - /** - * All records retrieved from Keeper Secrets Manager, where each key is the - * username of the corresponding record. The username of a record is - * determined by {@link Login} fields, thus a record may be associated with - * multiple users. If a record is associated with multiple users, there - * will be multiple references to that record within this Map. The contents - * of this Map are automatically updated if {@link #validateCache()} - * refreshes the cache. This Map must not be accessed without - * {@link #cacheLock} acquired appropriately. Before using a value from - * this Map, {@link #cachedAmbiguousUsernames} must first be checked to - * verify that there is indeed only one record associated with that user. - */ - private final Map cachedRecordsByUsername = new HashMap<>(); - - /** - * The set of all usernames that are associated with multiple records, and - * thus cannot uniquely identify a record. The contents of this Set are - * automatically updated if {@link #validateCache()} refreshes the cache. - * This Set must not be accessed without {@link #cacheLock} acquired - * appropriately.This Set must be checked before using a value retrieved - * from {@link #cachedRecordsByUsername}. - */ - private final Set cachedAmbiguousUsernames = new HashSet<>(); - - /** - * Validates that all cached data is current with respect to - * {@link #CACHE_INTERVAL}, refreshing data from the server as needed. - * - * @throws GuacamoleException - * If an error occurs preventing the cached data from being refreshed. - */ - private void validateCache() throws GuacamoleException { - - long currentTime = System.currentTimeMillis(); - - // Perform a read-only check that the cache has actually expired before - // continuing - cacheLock.readLock().lock(); - try { - if (currentTime - cacheTimestamp < CACHE_INTERVAL) - return; - } - finally { - cacheLock.readLock().unlock(); - } - - cacheLock.writeLock().lock(); - try { - - // Cache may have been updated since the read-only check. Re-verify - // that the cache has expired before continuing with a full refresh - if (currentTime - cacheTimestamp < CACHE_INTERVAL) - return; - - // Attempt to pull all records first, allowing that operation to - // succeed/fail BEFORE we clear out the last cached success - KeeperSecrets secrets = SecretsManager.getSecrets(ksmConfig); - List records = secrets.getRecords(); - - // Store all secrets within cache - cachedSecrets = secrets; - - // Clear unambiguous cache of all records by UID - cachedRecordsByUid.clear(); - - // Clear cache of host-based records - cachedAmbiguousHosts.clear(); - cachedRecordsByHost.clear(); - - // Clear cache of login-based records - cachedAmbiguousUsernames.clear(); - cachedRecordsByUsername.clear(); - - // Store all records, sorting each into host-based and login-based - // buckets - records.forEach(record -> { - - // Store based on UID ... - cachedRecordsByUid.put(record.getRecordUid(), record); - - // ... and hostname/address - String hostname = recordService.getHostname(record); - addRecordForHost(record, hostname); - - // Store based on username ONLY if no hostname (will otherwise - // result in ambiguous entries for servers tied to identical - // accounts) - if (hostname == null) - addRecordForLogin(record, recordService.getUsername(record)); - - }); - - // Cache has been refreshed - this.cacheTimestamp = System.currentTimeMillis(); - - } - finally { - cacheLock.writeLock().unlock(); - } - - } - - /** - * Associates the given record with the given hostname. The hostname may be - * null. Both {@link #cachedRecordsByHost} and {@link #cachedAmbiguousHosts} - * are updated appropriately. The write lock of {@link #cacheLock} must - * already be acquired before invoking this function. - * - * @param record - * The record to associate with the hosts in the given field. - * - * @param hostname - * The hostname/address that the given record should be associated - * with. This may be null. - */ - private void addRecordForHost(KeeperRecord record, String hostname) { - - if (hostname == null) - return; - - KeeperRecord existing = cachedRecordsByHost.putIfAbsent(hostname, record); - if (existing != null && record != existing) - cachedAmbiguousHosts.add(hostname); - - } - - /** - * Associates the given record with the given username. The given username - * may be null. Both {@link #cachedRecordsByUsername} and - * {@link #cachedAmbiguousUsernames} are updated appropriately. The write - * lock of {@link #cacheLock} must already be acquired before invoking this - * function. - * - * @param record - * The record to associate with the given username. - * - * @param username - * The username that the given record should be associated with. This - * may be null. - */ - private void addRecordForLogin(KeeperRecord record, String username) { - - if (username == null) - return; - - KeeperRecord existing = cachedRecordsByUsername.putIfAbsent(username, record); - if (existing != null && record != existing) - cachedAmbiguousUsernames.add(username); - - } - - /** - * Returns all records accessible via Keeper Secrets Manager. The records - * returned are arbitrarily ordered. - * - * @return - * An unmodifiable Collection of all records accessible via Keeper - * Secrets Manager, in no particular order. - * - * @throws GuacamoleException - * If an error occurs that prevents records from being retrieved. - */ - public Collection getRecords() throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { - return Collections.unmodifiableCollection(cachedRecordsByUid.values()); - } - finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Returns the record having the given UID. If no such record exists, null - * is returned. - * - * @param uid - * The UID of the record to return. - * - * @return - * The record having the given UID, or null if there is no such record. - * - * @throws GuacamoleException - * If an error occurs that prevents the record from being retrieved. - */ - public KeeperRecord getRecord(String uid) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { - return cachedRecordsByUid.get(uid); - } - finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Returns the record associated with the given hostname or IP address. If - * no such record exists, or there are multiple such records, null is - * returned. - * - * @param hostname - * The hostname of the record to return. - * - * @return - * The record associated with the given hostname, or null if there is - * no such record or multiple such records. - * - * @throws GuacamoleException - * If an error occurs that prevents the record from being retrieved. - */ - public KeeperRecord getRecordByHost(String hostname) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { - - if (cachedAmbiguousHosts.contains(hostname)) { - logger.debug("The hostname/address \"{}\" is referenced by " - + "multiple Keeper records and cannot be used to " - + "locate individual secrets.", hostname); - return null; - } - - return cachedRecordsByHost.get(hostname); - - } - finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Returns the record associated with the given username. If no such record - * exists, or there are multiple such records, null is returned. - * - * @param username - * The username of the record to return. - * - * @return - * The record associated with the given username, or null if there is - * no such record or multiple such records. - * - * @throws GuacamoleException - * If an error occurs that prevents the record from being retrieved. - */ - public KeeperRecord getRecordByLogin(String username) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { - - if (cachedAmbiguousUsernames.contains(username)) { - logger.debug("The username \"{}\" is referenced by multiple " - + "Keeper records and cannot be used to locate " - + "individual secrets.", username); - return null; - } - - return cachedRecordsByUsername.get(username); - - } - finally { - cacheLock.readLock().unlock(); - } - } - - /** - * Returns the value of the secret stored within Keeper Secrets Manager and - * represented by the given Keeper notation. Keeper notation locates the - * value of a specific field, custom field, or file associated with a - * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation - * - * @param notation - * The Keeper notation of the secret to retrieve. - * - * @return - * A Future which completes with the value of the secret represented by - * the given Keeper notation, or null if there is no such secret. - * - * @throws GuacamoleException - * If the requested secret cannot be retrieved or the Keeper notation - * is invalid. - */ - public Future getSecret(String notation) throws GuacamoleException { - validateCache(); - cacheLock.readLock().lock(); - try { - - // Retrieve any relevant file asynchronously - Matcher fileNotationMatcher = KEEPER_FILE_NOTATION.matcher(notation); - if (fileNotationMatcher.matches()) - return recordService.download(Notation.getFile(cachedSecrets, notation)); - - // Retrieve string values synchronously - return CompletableFuture.completedFuture(Notation.getValue(cachedSecrets, notation)); - - } - - // Unfortunately, the notation parser within the Keeper SDK throws - // plain Errors for retrieval failures ... - catch (Error e) { - logger.warn("Record \"{}\" does not exist.", notation); - logger.debug("Retrieval of record by Keeper notation failed.", e); - return CompletableFuture.completedFuture(null); - } - - // ... and plain Exceptions for parse failures (no subclasses) - catch (Exception e) { - logger.warn("\"{}\" is not valid Keeper notation. Please check " - + "the documentation at {} for valid formatting.", - notation, KEEPER_NOTATION_DOC_URL); - logger.debug("Provided Keeper notation could not be parsed.", e); - return CompletableFuture.completedFuture(null); - } - finally { - cacheLock.readLock().unlock(); - } - - } - -} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index ae80e68e2..be514ddfe 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -20,19 +20,33 @@ package org.apache.guacamole.vault.ksm.secret; import com.google.inject.Inject; -import com.google.inject.Singleton; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import com.keepersecurity.secretsManager.core.Hosts; import com.keepersecurity.secretsManager.core.KeeperRecord; +import com.keepersecurity.secretsManager.core.KeeperSecrets; +import com.keepersecurity.secretsManager.core.Login; +import com.keepersecurity.secretsManager.core.Notation; +import com.keepersecurity.secretsManager.core.SecretsManager; import com.keepersecurity.secretsManager.core.SecretsManagerOptions; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; - -import javax.annotation.Nullable; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Client which retrieves records from Keeper Secrets Manager, allowing @@ -42,88 +56,292 @@ import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; * information), it's not possible for the server to perform a search of * content on the client's behalf. The client has to perform its own search. */ -@Singleton public class KsmClient { /** - * Service for retrieving configuration information. + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(KsmClient.class); + + /** + * Service for retrieving data from records. */ @Inject - private KsmConfigurationService confService; + private KsmRecordService recordService; /** - * Factory for creating KSM cache instances for particular KSM configs. + * The publicly-accessible URL for Keeper's documentation covering Keeper + * notation. */ - @Inject - private KsmCacheFactory ksmCacheFactory; + private static final String KEEPER_NOTATION_DOC_URL = + "https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation"; /** - * A map of base-64 encoded JSON KSM config blobs to associated KSM cache instances. - * The `null` entry in this Map is associated with the KSM configuration parsed - * from the guacamole.properties config file. + * The regular expression that Keeper notation must match to be related to + * file retrieval. As the Keeper SDK provides mutually-exclusive for + * retrieving secret values and files via notation, the notation must first + * be tested to determine whether it refers to a file. */ - private final Map ksmCacheMap = new HashMap<>(); + private static final Pattern KEEPER_FILE_NOTATION = Pattern.compile("^(keeper://)?[^/]*/file/.+"); /** - * Create and return a KSM cache for the provided KSM config if not already - * present in the cache map, the existing cache entry. + * The maximum amount of time that an entry will be stored in the cache + * before being refreshed, in milliseconds. + */ + private static final long CACHE_INTERVAL = 5000; + + /** + * The KSM configuration associated with this client instance. + */ + private final SecretsManagerOptions ksmConfig; + + /** + * Read/write lock which guards access to all cached data, including the + * timestamp recording the last time the cache was refreshed. Readers of + * the cache must first acquire (and eventually release) the read lock, and + * writers of the cache must first acquire (and eventually release) the + * write lock. + */ + private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); + + /** + * The timestamp that the cache was last refreshed, in milliseconds, as + * returned by System.currentTimeMillis(). This value is automatically + * updated if {@link #validateCache()} refreshes the cache. This value must + * not be accessed without {@link #cacheLock} acquired appropriately. + */ + private volatile long cacheTimestamp = 0; + + /** + * The full cached set of secrets last retrieved from Keeper Secrets + * Manager. This value is automatically updated if {@link #validateCache()} + * refreshes the cache. This value must not be accessed without + * {@link #cacheLock} acquired appropriately. + */ + private KeeperSecrets cachedSecrets = null; + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * UID of the corresponding record. The contents of this Map are + * automatically updated if {@link #validateCache()} refreshes the cache. + * This Map must not be accessed without {@link #cacheLock} acquired + * appropriately. + */ + private final Map cachedRecordsByUid = new HashMap<>(); + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * hostname or IP address of the corresponding record. The hostname or IP + * address of a record is determined by {@link Hosts} fields, thus a record + * may be associated with multiple hosts. If a record is associated with + * multiple hosts, there will be multiple references to that record within + * this Map. The contents of this Map are automatically updated if + * {@link #validateCache()} refreshes the cache. This Map must not be + * accessed without {@link #cacheLock} acquired appropriately. Before using + * a value from this Map, {@link #cachedAmbiguousHosts} must first be + * checked to verify that there is indeed only one record associated with + * that host. + */ + private final Map cachedRecordsByHost = new HashMap<>(); + + /** + * The set of all hostnames or IP addresses that are associated with + * multiple records, and thus cannot uniquely identify a record. The + * contents of this Set are automatically updated if + * {@link #validateCache()} refreshes the cache. This Set must not be + * accessed without {@link #cacheLock} acquired appropriately.This Set + * must be checked before using a value retrieved from + * {@link #cachedRecordsByHost}. + */ + private final Set cachedAmbiguousHosts = new HashSet<>(); + + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * username of the corresponding record. The username of a record is + * determined by {@link Login} fields, thus a record may be associated with + * multiple users. If a record is associated with multiple users, there + * will be multiple references to that record within this Map. The contents + * of this Map are automatically updated if {@link #validateCache()} + * refreshes the cache. This Map must not be accessed without + * {@link #cacheLock} acquired appropriately. Before using a value from + * this Map, {@link #cachedAmbiguousUsernames} must first be checked to + * verify that there is indeed only one record associated with that user. + */ + private final Map cachedRecordsByUsername = new HashMap<>(); + + /** + * The set of all usernames that are associated with multiple records, and + * thus cannot uniquely identify a record. The contents of this Set are + * automatically updated if {@link #validateCache()} refreshes the cache. + * This Set must not be accessed without {@link #cacheLock} acquired + * appropriately.This Set must be checked before using a value retrieved + * from {@link #cachedRecordsByUsername}. + */ + private final Set cachedAmbiguousUsernames = new HashSet<>(); + + /** + * Create a new KSM client based around the provided KSM configuration. * * @param ksmConfig - * The base-64 encoded JSON KSM config blob associated with the cache entry. - * If an associated entry does not already exist, it will be created using - * this configuration. - * - * @return - * A KSM cache for the provided KSM config if not already present in the - * cache map, otherwise the existing cache entry. - * - * @throws GuacamoleException - * If an error occurs while creating the KSM cache. + * The KSM configuration to use when retrieving properties from KSM. */ - private KsmCache createCacheIfNeeded(@Nullable String ksmConfig) - throws GuacamoleException { - - // If a cache already exists for the provided config, use it - KsmCache ksmCache = ksmCacheMap.get(ksmConfig); - if (ksmCache != null) - return ksmCache; - - // Create and store a new KSM cache instance for the provided KSM config blob - SecretsManagerOptions options = confService.getSecretsManagerOptions(ksmConfig); - return ksmCacheMap.put(ksmConfig, ksmCacheFactory.create(options)); + @AssistedInject + public KsmClient(@Assisted SecretsManagerOptions ksmConfig) { + this.ksmConfig = ksmConfig; } /** - * Returns all records accessible via Keeper Secrets Manager, associated - * with the provided KSM config. If no KSM config is provided, records - * associated with the default configuration as retrieved from the config - * file will be used. The records returned are arbitrarily ordered. + * Validates that all cached data is current with respect to + * {@link #CACHE_INTERVAL}, refreshing data from the server as needed. * - * @param ksmConfig - * The base-64 encoded JSON KSM config blob associated associated with - * the KSM vault that should be used to fetch the records. + * @throws GuacamoleException + * If an error occurs preventing the cached data from being refreshed. + */ + private void validateCache() throws GuacamoleException { + + long currentTime = System.currentTimeMillis(); + + // Perform a read-only check that the cache has actually expired before + // continuing + cacheLock.readLock().lock(); + try { + if (currentTime - cacheTimestamp < CACHE_INTERVAL) + return; + } + finally { + cacheLock.readLock().unlock(); + } + + cacheLock.writeLock().lock(); + try { + + // Client may have been updated since the read-only check. Re-verify + // that the cache has expired before continuing with a full refresh + if (currentTime - cacheTimestamp < CACHE_INTERVAL) + return; + + // Attempt to pull all records first, allowing that operation to + // succeed/fail BEFORE we clear out the last cached success + KeeperSecrets secrets = SecretsManager.getSecrets(ksmConfig); + List records = secrets.getRecords(); + + // Store all secrets within cache + cachedSecrets = secrets; + + // Clear unambiguous cache of all records by UID + cachedRecordsByUid.clear(); + + // Clear cache of host-based records + cachedAmbiguousHosts.clear(); + cachedRecordsByHost.clear(); + + // Clear cache of login-based records + cachedAmbiguousUsernames.clear(); + cachedRecordsByUsername.clear(); + + // Store all records, sorting each into host-based and login-based + // buckets + records.forEach(record -> { + + // Store based on UID ... + cachedRecordsByUid.put(record.getRecordUid(), record); + + // ... and hostname/address + String hostname = recordService.getHostname(record); + addRecordForHost(record, hostname); + + // Store based on username ONLY if no hostname (will otherwise + // result in ambiguous entries for servers tied to identical + // accounts) + if (hostname == null) + addRecordForLogin(record, recordService.getUsername(record)); + + }); + + // Client has been refreshed + this.cacheTimestamp = System.currentTimeMillis(); + + } + finally { + cacheLock.writeLock().unlock(); + } + + } + + /** + * Associates the given record with the given hostname. The hostname may be + * null. Both {@link #cachedRecordsByHost} and {@link #cachedAmbiguousHosts} + * are updated appropriately. The write lock of {@link #cacheLock} must + * already be acquired before invoking this function. + * + * @param record + * The record to associate with the hosts in the given field. + * + * @param hostname + * The hostname/address that the given record should be associated + * with. This may be null. + */ + private void addRecordForHost(KeeperRecord record, String hostname) { + + if (hostname == null) + return; + + KeeperRecord existing = cachedRecordsByHost.putIfAbsent(hostname, record); + if (existing != null && record != existing) + cachedAmbiguousHosts.add(hostname); + + } + + /** + * Associates the given record with the given username. The given username + * may be null. Both {@link #cachedRecordsByUsername} and + * {@link #cachedAmbiguousUsernames} are updated appropriately. The write + * lock of {@link #cacheLock} must already be acquired before invoking this + * function. + * + * @param record + * The record to associate with the given username. + * + * @param username + * The username that the given record should be associated with. This + * may be null. + */ + private void addRecordForLogin(KeeperRecord record, String username) { + + if (username == null) + return; + + KeeperRecord existing = cachedRecordsByUsername.putIfAbsent(username, record); + if (existing != null && record != existing) + cachedAmbiguousUsernames.add(username); + + } + + /** + * Returns all records accessible via Keeper Secrets Manager. The records + * returned are arbitrarily ordered. * * @return * An unmodifiable Collection of all records accessible via Keeper - * Secrets Manager, in no particular order. + * Secrets Manager, in no particular order. * * @throws GuacamoleException * If an error occurs that prevents records from being retrieved. */ - public Collection getRecords( - @Nullable String ksmConfig) throws GuacamoleException { - - // Call through to the associated KSM cache instance - return createCacheIfNeeded(ksmConfig).getRecords(); + public Collection getRecords() throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + return Collections.unmodifiableCollection(cachedRecordsByUid.values()); + } + finally { + cacheLock.readLock().unlock(); + } } /** - * Returns the record having the given KSM config, and the given UID. - * If no such record exists, null is returned. - * - * @param ksmConfig - * The base-64 encoded JSON KSM config blob associated associated with - * the KSM vault that should be used to fetch the record. + * Returns the record having the given UID. If no such record exists, null + * is returned. * * @param uid * The UID of the record to return. @@ -134,21 +352,21 @@ public class KsmClient { * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ - public KeeperRecord getRecord( - @Nullable String ksmConfig, String uid) throws GuacamoleException { - - // Call through to the associated KSM cache instance - return createCacheIfNeeded(ksmConfig).getRecord(uid); + public KeeperRecord getRecord(String uid) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + return cachedRecordsByUid.get(uid); + } + finally { + cacheLock.readLock().unlock(); + } } /** - * Returns the record associated with the given hostname or IP address - * and the given KSM config. If no such record exists, or there are multiple - * such records, null is returned. - * - * @param ksmConfig - * The base-64 encoded JSON KSM config blob associated associated with - * the KSM vault that should be used to fetch the record. + * Returns the record associated with the given hostname or IP address. If + * no such record exists, or there are multiple such records, null is + * returned. * * @param hostname * The hostname of the record to return. @@ -160,22 +378,30 @@ public class KsmClient { * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ - public KeeperRecord getRecordByHost( - @Nullable String ksmConfig, String hostname) throws GuacamoleException { + public KeeperRecord getRecordByHost(String hostname) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { - // Call through to the associated KSM cache instance - return createCacheIfNeeded(ksmConfig).getRecordByHost(hostname); + if (cachedAmbiguousHosts.contains(hostname)) { + logger.debug("The hostname/address \"{}\" is referenced by " + + "multiple Keeper records and cannot be used to " + + "locate individual secrets.", hostname); + return null; + } + return cachedRecordsByHost.get(hostname); + + } + finally { + cacheLock.readLock().unlock(); + } } /** * Returns the record associated with the given username. If no such record * exists, or there are multiple such records, null is returned. * - * @param ksmConfig - * The base-64 encoded JSON KSM config blob associated associated with - * the KSM vault that should be used to fetch the record. - * * @param username * The username of the record to return. * @@ -186,24 +412,31 @@ public class KsmClient { * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ - public KeeperRecord getRecordByLogin( - @Nullable String ksmConfig, String username) throws GuacamoleException { + public KeeperRecord getRecordByLogin(String username) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { - // Call through to the associated KSM cache instance - return createCacheIfNeeded(ksmConfig).getRecordByLogin(username); + if (cachedAmbiguousUsernames.contains(username)) { + logger.debug("The username \"{}\" is referenced by multiple " + + "Keeper records and cannot be used to locate " + + "individual secrets.", username); + return null; + } + return cachedRecordsByUsername.get(username); + + } + finally { + cacheLock.readLock().unlock(); + } } /** - * Returns the value of the secret stored within the Keeper Secrets Manager vault - * associated with the provided KSM config, and represented by the given Keeper - * notation. Keeper notation locates the value of a specific field, custom field, - * or file associated with a specific record. - * See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation - * - * @param ksmConfig - * The base-64 encoded JSON KSM config blob associated associated with - * the KSM vault that should be used to fetch the record. + * Returns the value of the secret stored within Keeper Secrets Manager and + * represented by the given Keeper notation. Keeper notation locates the + * value of a specific field, custom field, or file associated with a + * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation * * @param notation * The Keeper notation of the secret to retrieve. @@ -216,11 +449,40 @@ public class KsmClient { * If the requested secret cannot be retrieved or the Keeper notation * is invalid. */ - public Future getSecret( - @Nullable String ksmConfig, String notation) throws GuacamoleException { + public Future getSecret(String notation) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { - // Call through to the associated KSM cache instance - return createCacheIfNeeded(ksmConfig).getSecret(notation); + // Retrieve any relevant file asynchronously + Matcher fileNotationMatcher = KEEPER_FILE_NOTATION.matcher(notation); + if (fileNotationMatcher.matches()) + return recordService.download(Notation.getFile(cachedSecrets, notation)); + + // Retrieve string values synchronously + return CompletableFuture.completedFuture(Notation.getValue(cachedSecrets, notation)); + + } + + // Unfortunately, the notation parser within the Keeper SDK throws + // plain Errors for retrieval failures ... + catch (Error e) { + logger.warn("Record \"{}\" does not exist.", notation); + logger.debug("Retrieval of record by Keeper notation failed.", e); + return CompletableFuture.completedFuture(null); + } + + // ... and plain Exceptions for parse failures (no subclasses) + catch (Exception e) { + logger.warn("\"{}\" is not valid Keeper notation. Please check " + + "the documentation at {} for valid formatting.", + notation, KEEPER_NOTATION_DOC_URL); + logger.debug("Provided Keeper notation could not be parsed.", e); + return CompletableFuture.completedFuture(null); + } + finally { + cacheLock.readLock().unlock(); + } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCacheFactory.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java similarity index 79% rename from extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCacheFactory.java rename to extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java index 4da9ee6bd..f8220c16f 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmCacheFactory.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java @@ -24,22 +24,22 @@ import javax.annotation.Nonnull; import com.keepersecurity.secretsManager.core.SecretsManagerOptions; /** - * Factory for creating KsmCache instances. + * Factory for creating KsmClient instances. */ -public interface KsmCacheFactory { +public interface KsmClientFactory { /** - * Returns a new instance of a KsmCache instance associated with + * Returns a new instance of a KsmClient instance associated with * the provided KSM configuration options. * * @param ksmConfigOptions - * The KSM config options to use when constructing the KsmCache + * The KSM config options to use when constructing the KsmClient * object. * * @return - * A new KsmCache instance associated with the provided KSM config + * A new KsmClient instance associated with the provided KSM config * options. */ - KsmCache create(@Nonnull SecretsManagerOptions ksmConfigOptions); + KsmClient create(@Nonnull SecretsManagerOptions ksmConfigOptions); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 21d7c91ac..6e2c8f201 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -22,31 +22,47 @@ package org.apache.guacamole.vault.ksm.secret; import com.google.inject.Inject; import com.google.inject.Singleton; import com.keepersecurity.secretsManager.core.KeeperRecord; +import com.keepersecurity.secretsManager.core.SecretsManagerOptions; + import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Future; +import javax.annotation.Nonnull; + import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.Connectable; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.protocol.GuacamoleConfiguration; import org.apache.guacamole.token.TokenFilter; +import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.secret.VaultSecretService; import org.apache.guacamole.vault.secret.WindowsUsername; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Service which retrieves secrets from Keeper Secrets Manager. + * The configuration used to connect to KSM can be set at a global + * level using guacamole.properties, or using a connection group + * attribute. */ @Singleton public class KsmSecretService implements VaultSecretService { /** - * Client for retrieving records and secrets from Keeper Secrets Manager. + * Logger for this class. */ - @Inject - private KsmClient ksm; + private static final Logger logger = LoggerFactory.getLogger(VaultSecretService.class); /** * Service for retrieving data from records. @@ -60,6 +76,53 @@ public class KsmSecretService implements VaultSecretService { @Inject private KsmConfigurationService confService; + /** + * Factory for creating KSM client instances. + */ + @Inject + private KsmClientFactory ksmClientFactory; + + /** + * A map of base-64 encoded JSON KSM config blobs to associated KSM client instances. + * The `null` entry in this Map is associated with the KSM configuration parsed + * from the guacamole.properties config file. A distinct KSM client will exist for + * every KSM config. + */ + private final ConcurrentMap ksmClientMap = new ConcurrentHashMap<>(); + + /** + * Create and return a KSM cache for the provided KSM config if not already + * present in the cache map, otherwise return the existing cache entry. + * + * @param ksmConfig + * The base-64 encoded JSON KSM config blob associated with the cache entry. + * If an associated entry does not already exist, it will be created using + * this configuration. + * + * @return + * A KSM cache for the provided KSM config if not already present in the + * cache map, otherwise the existing cache entry. + * + * @throws GuacamoleException + * If an error occurs while creating the KSM cache. + */ + private KsmClient getClient(@Nonnull String ksmConfig) + throws GuacamoleException { + + // If a cache already exists for the provided config, use it + KsmClient ksmClient = ksmClientMap.get(ksmConfig); + if (ksmClient != null) + return ksmClient; + + // Create and store a new KSM cache instance for the provided KSM config blob + SecretsManagerOptions options = confService.getSecretsManagerOptions(ksmConfig); + ksmClient = ksmClientFactory.create(options); + KsmClient prevClient = ksmClientMap.putIfAbsent(ksmConfig, ksmClient); + + // If the cache was already set before this thread got there, use the existing one + return prevClient != null ? prevClient : ksmClient; + } + @Override public String canonicalize(String nameComponent) { try { @@ -74,9 +137,21 @@ public class KsmSecretService implements VaultSecretService { } } + @Override + public Future getValue(UserContext userContext, Connectable connectable, + String name) throws GuacamoleException { + + // Attempt to find a KSM config for this connection or group + String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable); + + return getClient(ksmConfig).getSecret(name); + } + @Override public Future getValue(String name) throws GuacamoleException { - return ksm.getSecret(name); + + // Use the default KSM configuration from guacamole.properties + return getClient(confService.getKsmConfig()).getSecret(name); } /** @@ -153,13 +228,82 @@ public class KsmSecretService implements VaultSecretService { } + /** + * Search for a KSM configuration attribute, recursing up the connection group tree + * until a connection group with the appropriate attribute is found. If the KSM config + * is found, it will be returned. If not, the default value from the config file will + * be returned. + * + * @param userContext + * The userContext associated with the connection or connection group. + * + * @param connectable + * A connection or connection group for which the tokens are being replaced. + * + * @return + * The value of the KSM configuration attribute if found in the tree, the default + * KSM config blob defined in guacamole.properties otherwise. + * + * @throws GuacamoleException + * If an error occurs while attempting to retrieve the KSM config attribute, or if + * no KSM config is found in the connection group tree, and the value is also not + * defined in the config file. + */ + private String getConnectionGroupKsmConfig( + UserContext userContext, Connectable connectable) throws GuacamoleException { + + // Check to make sure it's a usable type before proceeding + if ( + !(connectable instanceof Connection) + && !(connectable instanceof ConnectionGroup)) { + logger.warn( + "Unsupported Connectable type: {}; skipping KSM config lookup.", + connectable.getClass()); + + // Use the default value if searching is impossible + return confService.getKsmConfig(); + } + + // For connections, start searching the parent group for the KSM config + // For connection groups, start searching the group directly + String parentIdentifier = (connectable instanceof Connection) + ? ((Connection) connectable).getParentIdentifier() + : ((ConnectionGroup) connectable).getIdentifier(); + + Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); + while (true) { + + // Fetch the parent group, if one exists + ConnectionGroup group = connectionGroupDirectory.get(parentIdentifier); + if (group == null) + break; + + // If the current connection group has the KSM configuration attribute, return immediately + String ksmConfig = group.getAttributes().get(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + if (ksmConfig != null) + return ksmConfig; + + // Otherwise, keep searching up the tree until an appropriate configuration is found + parentIdentifier = group.getParentIdentifier(); + } + + // If no KSM configuration was ever found, use the default value + return confService.getKsmConfig(); + } + @Override - public Map> getTokens(GuacamoleConfiguration config, - TokenFilter filter) throws GuacamoleException { + public Map> getTokens(UserContext userContext, Connectable connectable, + GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException { Map> tokens = new HashMap<>(); Map parameters = config.getParameters(); + // Attempt to find a KSM config for this connection or group + String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable); + + // Get a client instance for this KSM config + KsmClient ksm = getClient(ksmConfig); + // Retrieve and define server-specific tokens, if any String hostname = parameters.get("hostname"); if (hostname != null && !hostname.isEmpty()) From 025e831b3db8281241dbca407bacd8bfc0bea2e2 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 1 Jul 2022 21:57:47 +0000 Subject: [PATCH 029/289] GUACAMOLE-1372: Add configuration properties for setting private key and cert. --- .../auth/saml/conf/ConfigurationService.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java index 048773729..026f17363 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java @@ -24,7 +24,12 @@ import com.onelogin.saml2.settings.IdPMetadataParser; import com.onelogin.saml2.settings.Saml2Settings; import com.onelogin.saml2.settings.SettingsBuilder; import com.onelogin.saml2.util.Constants; + +import java.io.File; +import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.HashMap; import java.util.Map; import javax.ws.rs.core.UriBuilder; @@ -32,9 +37,12 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.BooleanGuacamoleProperty; +import org.apache.guacamole.properties.FileGuacamoleProperty; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.properties.URIGuacamoleProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Service for retrieving configuration information regarding the SAML @@ -42,6 +50,11 @@ import org.apache.guacamole.properties.URIGuacamoleProperty; */ public class ConfigurationService { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(ConfigurationService.class); + /** * The URI of the file containing the XML Metadata associated with the * SAML IdP. @@ -161,6 +174,30 @@ public class ConfigurationService { }; + /** + * The file containing the X.509 cert to use when signing or encrypting + * requests to the SAML IdP. + */ + private static final FileGuacamoleProperty SAML_X509_CERT_PATH = + new FileGuacamoleProperty() { + + @Override + public String getName() { return "saml-x509-cert-path"; } + + }; + + /** + * The file containing the private key to use when signing or encrypting + * requests to the SAML IdP. + */ + private static final FileGuacamoleProperty SAML_PRIVATE_KEY_PATH = + new FileGuacamoleProperty() { + + @Override + public String getName() { return "saml-private-key-path"; } + + }; + /** * The Guacamole server environment. */ @@ -329,6 +366,69 @@ public class ConfigurationService { return environment.getProperty(SAML_AUTH_TIMEOUT, 5); } + /** + * Returns the file containing the X.509 certificate to use when signing + * requests to the SAML IdP. If the property is not set, null will be + * returned. + * + * @return + * The file containing the X.509 certificate to use when signing + * requests to the SAML IdP, or null if not defined. + * + * @throws GuacamoleException + * If the X.509 certificate cannot be parsed. + */ + public File getCertificateFile() throws GuacamoleException { + return environment.getProperty(SAML_X509_CERT_PATH); + } + + /** + * Returns the file containing the private key to use when signing + * requests to the SAML IdP. If the property is not set, null will be + * returned. + * + * @return + * The file containing the private key to use when signing + * requests to the SAML IdP, or null if not defined. + * + * @throws GuacamoleException + * If the private key file cannot be parsed. + */ + public File getPrivateKeyFile() throws GuacamoleException { + return environment.getProperty(SAML_PRIVATE_KEY_PATH); + } + + /** + * Returns the contents of a small file, such as a private key or certificate into + * a String. If the file does not exist, or cannot be read for any reason, a warning + * will be logged and null will be returned. + * + * @param file + * The file to read into a string. + * + * @param name + * A human-readable name for the file, to be used when formatting log messages. + * + * @return + * The contents of the file having the given path, or null if the file does not + * exist or cannot be read. + */ + private String readFileContentsIntoString(File file, String name) { + + // Attempt to read the file directly into a String + try { + return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + } + + // If the file cannot be read, log a warning and treat it as if it does not exist + catch (IOException e) { + logger.warn("{} \"{}\" could not be read: {}.", name, file, e.getMessage()); + logger.debug("{} \"{}\" could not be read.", name, file, e); + return null; + } + + } + /** * Returns the collection of SAML settings used to initialize the client. * @@ -380,6 +480,18 @@ public class ConfigurationService { UriBuilder.fromUri(getCallbackUrl()).path("api/ext/saml/callback").build().toString()); } + // If a private key file is set, load the value into the builder now + File privateKeyFile = getPrivateKeyFile(); + if (privateKeyFile != null) + samlMap.put(SettingsBuilder.SP_PRIVATEKEY_PROPERTY_KEY, + readFileContentsIntoString(privateKeyFile, "Private Key")); + + // If a certificate file is set, load the value into the builder now + File certificateFile = getCertificateFile(); + if (certificateFile != null) + samlMap.put(SettingsBuilder.SP_X509CERT_PROPERTY_KEY, + readFileContentsIntoString(certificateFile, "X.509 Certificate")); + SettingsBuilder samlBuilder = new SettingsBuilder(); Saml2Settings samlSettings = samlBuilder.fromValues(samlMap).build(); samlSettings.setStrict(getStrict()); From 616cb3968270be4fc6cc96c72ed967e27872fcfb Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 5 Jul 2022 20:37:05 +0000 Subject: [PATCH 030/289] GUACAMOLE-1372: Throw fatal exception if files are specified but unreadable. --- .../auth/saml/conf/ConfigurationService.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java index 026f17363..95862bd34 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java @@ -41,8 +41,6 @@ import org.apache.guacamole.properties.FileGuacamoleProperty; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.properties.URIGuacamoleProperty; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Service for retrieving configuration information regarding the SAML @@ -50,11 +48,6 @@ import org.slf4j.LoggerFactory; */ public class ConfigurationService { - /** - * Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger(ConfigurationService.class); - /** * The URI of the file containing the XML Metadata associated with the * SAML IdP. @@ -400,8 +393,8 @@ public class ConfigurationService { /** * Returns the contents of a small file, such as a private key or certificate into - * a String. If the file does not exist, or cannot be read for any reason, a warning - * will be logged and null will be returned. + * a String. If the file does not exist, or cannot be read for any reason, an exception + * will be thrown with the details of the failure. * * @param file * The file to read into a string. @@ -410,10 +403,12 @@ public class ConfigurationService { * A human-readable name for the file, to be used when formatting log messages. * * @return - * The contents of the file having the given path, or null if the file does not - * exist or cannot be read. + * The contents of the file having the given path. + * + * @throws GuacamoleException + * If the provided file does not exist, or cannot be read for any reason. */ - private String readFileContentsIntoString(File file, String name) { + private String readFileContentsIntoString(File file, String name) throws GuacamoleException { // Attempt to read the file directly into a String try { @@ -422,9 +417,8 @@ public class ConfigurationService { // If the file cannot be read, log a warning and treat it as if it does not exist catch (IOException e) { - logger.warn("{} \"{}\" could not be read: {}.", name, file, e.getMessage()); - logger.debug("{} \"{}\" could not be read.", name, file, e); - return null; + throw new GuacamoleServerException( + name + " at \"" + file.getAbsolutePath() + "\" could not be read.", e); } } From 5b69bf405d034258e1dd8ce34819add0dc2992ae Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 5 Jul 2022 17:59:44 +0000 Subject: [PATCH 031/289] GUACAMOLE-1629: Use TextField for KSM configuration since it's always one line. --- .../guacamole/vault/ksm/conf/KsmAttributeService.java | 4 ++-- .../apache/guacamole/vault/ksm/secret/KsmClient.java | 4 ++-- .../guacamole/vault/ksm/secret/KsmSecretService.java | 10 ++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java index ade0f97e9..83ab9c4a3 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java @@ -24,7 +24,7 @@ import java.util.Collection; import java.util.Collections; import org.apache.guacamole.form.Form; -import org.apache.guacamole.form.MultilineField; +import org.apache.guacamole.form.TextField; import org.apache.guacamole.vault.conf.VaultAttributeService; import com.google.inject.Singleton; @@ -47,7 +47,7 @@ public class KsmAttributeService implements VaultAttributeService { * per-connection-group basis. */ public static final Form KSM_CONFIGURATION_FORM = new Form("ksm-config", - Arrays.asList(new MultilineField(KSM_CONFIGURATION_ATTRIBUTE))); + Arrays.asList(new TextField(KSM_CONFIGURATION_ATTRIBUTE))); /** * All KSM-specific connection group attributes, organized by form. diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index be514ddfe..5572ef71d 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -215,7 +215,7 @@ public class KsmClient { cacheLock.writeLock().lock(); try { - // Client may have been updated since the read-only check. Re-verify + // Cache may have been updated since the read-only check. Re-verify // that the cache has expired before continuing with a full refresh if (currentTime - cacheTimestamp < CACHE_INTERVAL) return; @@ -258,7 +258,7 @@ public class KsmClient { }); - // Client has been refreshed + // Cache has been refreshed this.cacheTimestamp = System.currentTimeMillis(); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 6e2c8f201..bea98d934 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -84,14 +84,12 @@ public class KsmSecretService implements VaultSecretService { /** * A map of base-64 encoded JSON KSM config blobs to associated KSM client instances. - * The `null` entry in this Map is associated with the KSM configuration parsed - * from the guacamole.properties config file. A distinct KSM client will exist for - * every KSM config. + * A distinct KSM client will exist for every KSM config. */ private final ConcurrentMap ksmClientMap = new ConcurrentHashMap<>(); /** - * Create and return a KSM cache for the provided KSM config if not already + * Create and return a KSM client for the provided KSM config if not already * present in the cache map, otherwise return the existing cache entry. * * @param ksmConfig @@ -100,11 +98,11 @@ public class KsmSecretService implements VaultSecretService { * this configuration. * * @return - * A KSM cache for the provided KSM config if not already present in the + * A KSM client for the provided KSM config if not already present in the * cache map, otherwise the existing cache entry. * * @throws GuacamoleException - * If an error occurs while creating the KSM cache. + * If an error occurs while creating the KSM client. */ private KsmClient getClient(@Nonnull String ksmConfig) throws GuacamoleException { From 374f1b5e494c968166fe3eb9a66b6f7769981400 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 6 Jul 2022 17:55:28 +0000 Subject: [PATCH 032/289] GUACAMOLE-1629: Always include any pre-existing connection group attributes when exposing new ones. --- .../guacamole/vault/conf/VaultAttributeService.java | 7 +++++-- .../apache/guacamole/vault/user/VaultUserContext.java | 10 +++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java index b77601621..f4129bb95 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java @@ -31,10 +31,13 @@ import org.apache.guacamole.form.Form; public interface VaultAttributeService { /** - * Return all connection group attributes to be exposed through the admin UI. + * Return all custom connection group attributes to be exposed through the + * admin UI for the current vault implementation. * * @return - * All connection group attributes to be exposed through the admin UI. + * All custom connection group attributes to be exposed through the + * admin UI for the current vault implementation. + * */ public Collection getConnectionGroupAttributes(); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index 876d7d186..06ee8aaac 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java @@ -28,6 +28,9 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.form.Form; @@ -425,7 +428,12 @@ public class VaultUserContext extends TokenInjectingUserContext { @Override public Collection getConnectionGroupAttributes() { - return attributeService.getConnectionGroupAttributes(); + + // Add any custom attributes to any previously defined attributes + return Stream.concat( + super.getConnectionGroupAttributes().stream(), + attributeService.getConnectionGroupAttributes().stream() + ).collect(Collectors.toUnmodifiableList()); } } From 0585ab5e5bc505d1338a419a19ea1b5484792738 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 6 Jul 2022 19:01:33 +0000 Subject: [PATCH 033/289] GUACAMOLE-1629: Fix client/cache confusion in comments. --- .../guacamole/vault/conf/VaultAttributeService.java | 1 - .../guacamole/vault/ksm/secret/KsmSecretService.java | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java index f4129bb95..2bd08cde6 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java @@ -37,7 +37,6 @@ public interface VaultAttributeService { * @return * All custom connection group attributes to be exposed through the * admin UI for the current vault implementation. - * */ public Collection getConnectionGroupAttributes(); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index bea98d934..2436a4e62 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -90,16 +90,16 @@ public class KsmSecretService implements VaultSecretService { /** * Create and return a KSM client for the provided KSM config if not already - * present in the cache map, otherwise return the existing cache entry. + * present in the client map, otherwise return the existing client entry. * * @param ksmConfig - * The base-64 encoded JSON KSM config blob associated with the cache entry. + * The base-64 encoded JSON KSM config blob associated with the client entry. * If an associated entry does not already exist, it will be created using * this configuration. * * @return * A KSM client for the provided KSM config if not already present in the - * cache map, otherwise the existing cache entry. + * client map, otherwise the existing client entry. * * @throws GuacamoleException * If an error occurs while creating the KSM client. @@ -107,17 +107,17 @@ public class KsmSecretService implements VaultSecretService { private KsmClient getClient(@Nonnull String ksmConfig) throws GuacamoleException { - // If a cache already exists for the provided config, use it + // If a client already exists for the provided config, use it KsmClient ksmClient = ksmClientMap.get(ksmConfig); if (ksmClient != null) return ksmClient; - // Create and store a new KSM cache instance for the provided KSM config blob + // Create and store a new KSM client instance for the provided KSM config blob SecretsManagerOptions options = confService.getSecretsManagerOptions(ksmConfig); ksmClient = ksmClientFactory.create(options); KsmClient prevClient = ksmClientMap.putIfAbsent(ksmConfig, ksmClient); - // If the cache was already set before this thread got there, use the existing one + // If the client was already set before this thread got there, use the existing one return prevClient != null ? prevClient : ksmClient; } From 12832bed88caff888e1193d109d58ca1d7e9905e Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 6 Jul 2022 19:06:42 +0000 Subject: [PATCH 034/289] GUACAMOLE-1629: Use Java 8 compatible collector to append connection group attributes. --- .../org/apache/guacamole/vault/user/VaultUserContext.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index 06ee8aaac..dbfbbb9cb 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java @@ -24,6 +24,7 @@ import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -430,10 +431,11 @@ public class VaultUserContext extends TokenInjectingUserContext { public Collection getConnectionGroupAttributes() { // Add any custom attributes to any previously defined attributes - return Stream.concat( + return Collections.unmodifiableCollection(Stream.concat( super.getConnectionGroupAttributes().stream(), attributeService.getConnectionGroupAttributes().stream() - ).collect(Collectors.toUnmodifiableList()); + ).collect(Collectors.toList())); + } } From b8058e7561d16f061ab025fbb973e7ad560b058d Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 20 Jul 2022 00:18:26 +0000 Subject: [PATCH 035/289] GUACAMOLE-1643: Validate/translate KSM configs and one-time tokens on connection group save. --- .../vault/user/VaultDirectoryService.java | 140 ++++++++++ .../vault/user/VaultUserContext.java | 54 ++++ .../ksm/KsmAuthenticationProviderModule.java | 3 + .../vault/ksm/user/KsmDirectoryService.java | 253 ++++++++++++++++++ .../src/main/resources/translations/en.json | 5 +- 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultDirectoryService.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultDirectoryService.java new file mode 100644 index 000000000..700d9d3be --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultDirectoryService.java @@ -0,0 +1,140 @@ +/* + * 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.vault.user; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.ActiveConnection; +import org.apache.guacamole.net.auth.Connection; +import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.SharingProfile; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.UserGroup; + +/** + * A service that allows a vault implementation to override the directory + * for any entity that a user context may return. + */ +public abstract class VaultDirectoryService { + + /** + * Given an existing User Directory, return a new Directory for + * this vault implementation. + * + * @return + * A new User Directory based on the provided Directory. + * + * @throws GuacamoleException + * If an error occurs while creating the Directory. + */ + public Directory getUserDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // By default, the provided directly will be returned unchanged + return underlyingDirectory; + } + + /** + * Given an existing UserGroup Directory, return a new Directory for + * this vault implementation. + * + * @return + * A new UserGroup Directory based on the provided Directory. + * + * @throws GuacamoleException + * If an error occurs while creating the Directory. + */ + public Directory getUserGroupDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // Unless overriden in the vault implementation, the underlying directory + // will be returned directly + return underlyingDirectory; + } + + /** + * Given an existing Connection Directory, return a new Directory for + * this vault implementation. + * + * @return + * A new Connection Directory based on the provided Directory. + * + * @throws GuacamoleException + * If an error occurs while creating the Directory. + */ + public Directory getConnectionDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // By default, the provided directly will be returned unchanged + return underlyingDirectory; + } + + /** + * Given an existing ConnectionGroup Directory, return a new Directory for + * this vault implementation. + * + * @return + * A new ConnectionGroup Directory based on the provided Directory. + * + * @throws GuacamoleException + * If an error occurs while creating the Directory. + */ + public Directory getConnectionGroupDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // By default, the provided directly will be returned unchanged + return underlyingDirectory; + } + + /** + * Given an existing ActiveConnection Directory, return a new Directory for + * this vault implementation. + * + * @return + * A new ActiveConnection Directory based on the provided Directory. + * + * @throws GuacamoleException + * If an error occurs while creating the Directory. + */ + public Directory getActiveConnectionDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // By default, the provided directly will be returned unchanged + return underlyingDirectory; + } + + /** + * Given an existing SharingProfile Directory, return a new Directory for + * this vault implementation. + * + * @return + * A new SharingProfile Directory based on the provided Directory. + * + * @throws GuacamoleException + * If an error occurs while creating the Directory. + */ + public Directory getSharingProfileDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // By default, the provided directly will be returned unchanged + return underlyingDirectory; + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index dbfbbb9cb..8e8f66853 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java @@ -35,11 +35,16 @@ import java.util.stream.Stream; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.form.Form; +import org.apache.guacamole.net.auth.ActiveConnection; import org.apache.guacamole.net.auth.Connectable; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.SharingProfile; import org.apache.guacamole.net.auth.TokenInjectingUserContext; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.protocol.GuacamoleConfiguration; import org.apache.guacamole.token.GuacamoleTokenUndefinedException; import org.apache.guacamole.token.TokenFilter; @@ -137,6 +142,13 @@ public class VaultUserContext extends TokenInjectingUserContext { @Inject private VaultAttributeService attributeService; + /** + * Service for modifying any underlying directories for the current + * vault implementation. + */ + @Inject + private VaultDirectoryService directoryService; + /** * Creates a new VaultUserContext which automatically injects tokens * containing values of secrets retrieved from a vault. The given @@ -438,4 +450,46 @@ public class VaultUserContext extends TokenInjectingUserContext { } + @Override + public Directory getUserDirectory() throws GuacamoleException { + + // Defer to the vault-specific directory service + return directoryService.getUserDirectory(super.getUserDirectory()); + } + + @Override + public Directory getUserGroupDirectory() throws GuacamoleException { + + // Defer to the vault-specific directory service + return directoryService.getUserGroupDirectory(super.getUserGroupDirectory()); + } + + @Override + public Directory getConnectionDirectory() throws GuacamoleException { + + // Defer to the vault-specific directory service + return directoryService.getConnectionDirectory(super.getConnectionDirectory()); + } + + @Override + public Directory getConnectionGroupDirectory() throws GuacamoleException { + + // Defer to the vault-specific directory service + return directoryService.getConnectionGroupDirectory(super.getConnectionGroupDirectory()); + } + + @Override + public Directory getActiveConnectionDirectory() throws GuacamoleException { + + // Defer to the vault-specific directory service + return directoryService.getActiveConnectionDirectory(super.getActiveConnectionDirectory()); + } + + @Override + public Directory getSharingProfileDirectory() throws GuacamoleException { + + // Defer to the vault-specific directory service + return directoryService.getSharingProfileDirectory(super.getSharingProfileDirectory()); + } + } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java index 6a7a70c83..3c28553e0 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java @@ -24,12 +24,14 @@ import org.apache.guacamole.vault.VaultAuthenticationProviderModule; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmSecretService; +import org.apache.guacamole.vault.ksm.user.KsmDirectoryService; import org.apache.guacamole.vault.conf.VaultAttributeService; import org.apache.guacamole.vault.conf.VaultConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmClient; import org.apache.guacamole.vault.ksm.secret.KsmClientFactory; import org.apache.guacamole.vault.ksm.secret.KsmRecordService; import org.apache.guacamole.vault.secret.VaultSecretService; +import org.apache.guacamole.vault.user.VaultDirectoryService; import com.google.inject.assistedinject.FactoryModuleBuilder; @@ -58,6 +60,7 @@ public class KsmAuthenticationProviderModule bind(VaultAttributeService.class).to(KsmAttributeService.class); bind(VaultConfigurationService.class).to(KsmConfigurationService.class); bind(VaultSecretService.class).to(KsmSecretService.class); + bind(VaultDirectoryService.class).to(KsmDirectoryService.class); // Bind factory for creating KSM Clients install(new FactoryModuleBuilder() diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java new file mode 100644 index 000000000..fc4a96213 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java @@ -0,0 +1,253 @@ +/* + * 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.vault.ksm.user; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.language.TranslatableGuacamoleClientException; +import org.apache.guacamole.net.auth.Attributes; +import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.DelegatingDirectory; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; +import org.apache.guacamole.vault.ksm.conf.KsmConfig; +import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; +import org.apache.guacamole.vault.user.VaultDirectoryService; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Inject; +import com.keepersecurity.secretsManager.core.InMemoryStorage; +import com.keepersecurity.secretsManager.core.SecretsManager; +import com.keepersecurity.secretsManager.core.SecretsManagerOptions; + +/** + * A KSM-specific vault directory service that wraps the connection group directory + * to enable automatic translation of KSM one-time tokens into base64-encoded JSON + * config bundles. + */ +public class KsmDirectoryService extends VaultDirectoryService { + + /** + * Service for retrieving KSM configuration details. + */ + @Inject + private KsmConfigurationService configurationService; + + /** + * A singleton ObjectMapper for converting a Map to a JSON string when + * generating a base64-encoded JSON KSM config blob. + */ + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * All expected fields in the KSM configuration JSON blob. + */ + private static final List EXPECTED_KSM_FIELDS = ( + Collections.unmodifiableList(Arrays.asList( + SecretsManager.KEY_HOSTNAME, + SecretsManager.KEY_CLIENT_ID, + SecretsManager.KEY_PRIVATE_KEY, + SecretsManager.KEY_CLIENT_KEY, + SecretsManager.KEY_APP_KEY, + SecretsManager.KEY_OWNER_PUBLIC_KEY, + SecretsManager.KEY_SERVER_PUBIC_KEY_ID + ))); + + /** + * Return true if the provided input is a valid base64-encoded string, + * false otherwise. + * + * @param input + * The string to check if base-64 encoded. + * + * @return + * true if the provided input is a valid base64-encoded string, + * false otherwise. + */ + private static boolean isBase64(String input) { + + try { + Base64.getDecoder().decode(input); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * Given an attributes-enabled entity, check for the presence of the + * KSM_CONFIGURATION_ATTRIBUTE attribute. If it's set, check if it's a valid + * KSM one-time token. If so, attempt to translate it to a base-64-encoded + * json KSM config blob, and set it back to the provided entity. + * If it's already a KSM config blob, validate it as config blob. If either + * validation fails, a GuacamoleException will be thrown. + * + * @param entity + * The attributes-enabled entity for which the KSM configuration + * attribute parsing/validation should be performed. + * + * @throws GuacamoleException + * If the KSM_CONFIGURATION_ATTRIBUTE is set, but fails to validate as + * either a KSM one-time-token, or a KSM base64-encoded JSON config blob. + */ + public void processAttributes(Attributes entity) throws GuacamoleException { + + // By default, if the KSM config attribute isn't being set, pass the + // provided attributes through without any changes + Map attributes = entity.getAttributes(); + + // Get the value of the KSM config attribute in the provided map + String ksmConfigValue = attributes.get( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + + // Check if the attribute is set to a non-empty value + if (ksmConfigValue != null && !ksmConfigValue.trim().isEmpty()) { + + // If it's already base64-encoded, it's a KSM configuration blob, + // so validate it immediately + if (isBase64(ksmConfigValue)) { + + // Attempt to validate the config as a base64-econded KSM config blob + try { + KsmConfig.parseKsmConfig(ksmConfigValue); + + // If it validates, the entity can be left alone - it's already valid + return; + } + + catch (GuacamoleException exception) { + + // If the parsing attempt fails, throw a translatable error for display + // on the frontend + throw new TranslatableGuacamoleClientException( + "Invalid base64-encoded JSON KSM config provided for " + + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute", + "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_CONFIG_BLOB", + exception); + } + } + + // It wasn't a valid base64-encoded string, it should be a one-time token, so + // attempt to validat it as such, and if valid, update the attribute to the + // base64 config blob generated by the token + try { + + // Create an initially empty storage to be populated using the one-time token + InMemoryStorage storage = new InMemoryStorage(); + + // Populate the in-memory storage using the one-time-token + SecretsManager.initializeStorage(storage, ksmConfigValue, null); + + // Create an options object using the values we extracted from the one-time token + SecretsManagerOptions options = new SecretsManagerOptions( + storage, null, + configurationService.getAllowUnverifiedCertificate()); + + // Attempt to fetch secrets using the options we created. This will both validate + // that the configuration works, and potentially populate missing fields that the + // initializeStorage() call did not set. + SecretsManager.getSecrets(options); + + // Create a map to store the extracted values from the KSM storage + Map configMap = new HashMap<>(); + + // Go through all the expected fields, extract from the KSM storage, + // and write to the newly created map + EXPECTED_KSM_FIELDS.forEach(configKey -> { + + // Only write the value into the new map if non-null + String value = storage.getString(configKey); + if (value != null) + configMap.put(configKey, value); + + }); + + // JSON-encode the value, and then base64 encode that to get the format + // that KSM would expect + String jsonString = objectMapper.writeValueAsString(configMap); + String base64EncodedJson = Base64.getEncoder().encodeToString( + jsonString.getBytes(StandardCharsets.UTF_8)); + + // Finally, try to parse the newly generated token as a KSM config. If this + // works, the config should be fully functional + KsmConfig.parseKsmConfig(base64EncodedJson); + + // Make a copy of the existing attributes, modifying just the value for + // KSM_CONFIGURATION_ATTRIBUTE + attributes = new HashMap<>(attributes); + attributes.put( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, base64EncodedJson); + + // Set the newly updated attributes back to the original object + entity.setAttributes(attributes); + + } + + // The KSM SDK only throws raw Exceptions, so we can't be more specific + catch (Exception exception) { + + // If the parsing attempt fails, throw a translatable error for display + // on the frontend + throw new TranslatableGuacamoleClientException( + "Invalid one-time KSM token provided for " + + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute", + "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_ONE_TIME_TOKEN", + exception); + } + } + + } + + @Override + public Directory getConnectionGroupDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // A ConnectionGroup directory that will intercept add and update calls to + // validate KSM configurations, and translate one-time-tokens, if possible + return new DelegatingDirectory(underlyingDirectory) { + + @Override + public void add(ConnectionGroup connectionGroup) throws GuacamoleException { + + // Check for the KSM config attribute and translate the one-time token + // if possible before adding + processAttributes(connectionGroup); + super.add(connectionGroup); + } + + @Override + public void update(ConnectionGroup connectionGroup) throws GuacamoleException { + + // Check for the KSM config attribute and translate the one-time token + // if possible before updating + processAttributes(connectionGroup); + super.update(connectionGroup); + } + + }; + } +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json index abda16cef..4601a13f1 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json @@ -6,7 +6,10 @@ "CONNECTION_GROUP_ATTRIBUTES" : { "SECTION_HEADER_KSM_CONFIG" : "Keeper Secrets Manager", - "FIELD_HEADER_KSM_CONFIG" : "KSM Service Configuration " + "FIELD_HEADER_KSM_CONFIG" : "KSM Service Configuration ", + + "ERROR_INVALID_KSM_CONFIG_BLOB" : "The provided base64-encoded KSM configuration blob is not valid. Please ensure that you have copied the entire blob.", + "ERROR_INVALID_KSM_ONE_TIME_TOKEN" : "The provided configuration is not a valid KSM one-time token or base64-encoded configuration blob. Please ensure that you have copied the entire token value." } } From 492dbf48d609a7ff270d33a196a07fa2e3af0406 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 21 Jul 2022 23:25:25 +0000 Subject: [PATCH 036/289] GUACAMOLE-1643: Ensure connection groups with an empty KSM config attribute fall back to the global config. --- .../apache/guacamole/vault/ksm/secret/KsmSecretService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 2436a4e62..3d0d65bd4 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -276,9 +276,10 @@ public class KsmSecretService implements VaultSecretService { if (group == null) break; - // If the current connection group has the KSM configuration attribute, return immediately + // If the current connection group has the KSM configuration attribute + // set to a non-empty value, return immediately String ksmConfig = group.getAttributes().get(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); - if (ksmConfig != null) + if (ksmConfig != null && !ksmConfig.trim().isEmpty()) return ksmConfig; // Otherwise, keep searching up the tree until an appropriate configuration is found @@ -287,6 +288,7 @@ public class KsmSecretService implements VaultSecretService { // If no KSM configuration was ever found, use the default value return confService.getKsmConfig(); + } @Override From d599ad317ce1d55db1a0a7ca78594a04ea3281c1 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 21 Jul 2022 23:26:25 +0000 Subject: [PATCH 037/289] GUACAMOLE-1643: Ensure that the KSM config attribute is always visible for connection groups. --- .../vault/ksm/user/KsmConnectionGroup.java | 78 +++++++++++++++++++ .../vault/ksm/user/KsmDirectoryService.java | 25 +++++- 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java new file mode 100644 index 000000000..397c42f11 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java @@ -0,0 +1,78 @@ +/* + * 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.vault.ksm.user; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.DelegatingConnectionGroup; +import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; + + /** + * A KSM-specific connection group implementation that always exposes + * the KSM_CONFIGURATION_ATTRIBUTE attribute, even when no value is set. + * This ensures that the attribute will always show up in the UI, even + * for connection groups that don't already have it set. + */ + public class KsmConnectionGroup extends DelegatingConnectionGroup { + + /** + * Create a new KsmConnectionGroup instance, wrapping the provided + * ConnectionGroup. + * + * @param connectionGroup + * The ConnectionGroup instance to wrap. + */ + public KsmConnectionGroup(ConnectionGroup connectionGroup) { + + // Wrap the provided connection group + super(connectionGroup); + } + + @Override + public Map getAttributes() { + + // All attributes defined on the underlying connection group + Map attributes = super.getAttributes(); + + // If the attribute is already present, there's no need to add it - return + // the existing attributes as they are + if (attributes.containsKey(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE)) + return attributes; + + // Make a copy of the existing attributes and add KSM_CONFIGURATION_ATTRIBUTE + attributes = new HashMap<>(attributes); + attributes.put(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); + return attributes; + + } + + /** + * Return the underlying ConnectionGroup that's wrapped by this KsmConnectionGroup. + * + * @return + * The underlying ConnectionGroup that's wrapped by this KsmConnectionGroup. + */ + ConnectionGroup getUnderlyConnectionGroup() { + return getDelegateConnectionGroup(); + } + + } \ No newline at end of file diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java index fc4a96213..b924bc524 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java @@ -31,7 +31,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.net.auth.Attributes; import org.apache.guacamole.net.auth.ConnectionGroup; -import org.apache.guacamole.net.auth.DelegatingDirectory; +import org.apache.guacamole.net.auth.DecoratingDirectory; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfig; @@ -227,8 +227,10 @@ public class KsmDirectoryService extends VaultDirectoryService { Directory underlyingDirectory) throws GuacamoleException { // A ConnectionGroup directory that will intercept add and update calls to - // validate KSM configurations, and translate one-time-tokens, if possible - return new DelegatingDirectory(underlyingDirectory) { + // validate KSM configurations, and translate one-time-tokens, if possible, + // as well as ensuring that all ConnectionGroups returned include the + // KSM_CONFIGURATION_ATTRIBUTE attribute, so it will be available in the UI. + return new DecoratingDirectory(underlyingDirectory) { @Override public void add(ConnectionGroup connectionGroup) throws GuacamoleException { @@ -248,6 +250,23 @@ public class KsmDirectoryService extends VaultDirectoryService { super.update(connectionGroup); } + @Override + protected ConnectionGroup decorate(ConnectionGroup connectionGroup) throws GuacamoleException { + + // Wrap the existing connection group in a KsmConnection to ensure the presence of the + // KSM_CONFIGURATION_ATTRIBUTE attribute + return new KsmConnectionGroup(connectionGroup); + + } + + @Override + protected ConnectionGroup undecorate(ConnectionGroup connectionGroup) throws GuacamoleException { + + // Return the underlying connection group that the KsmConnectionGroup wraps + return ((KsmConnectionGroup) connectionGroup).getUnderlyConnectionGroup(); + + } + }; } } From 67b5db77e1684c0a70ceed795cc5f3d79eaa94c8 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 26 Jul 2022 21:39:07 +0000 Subject: [PATCH 038/289] GUACAMOLE-1629: Keep track of iterated identifiers when recursing connection group tree looking for KSM attribute to ensure no infinite loop. --- .../vault/ksm/secret/KsmSecretService.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 3d0d65bd4..d8168dc8f 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -27,7 +27,9 @@ import com.keepersecurity.secretsManager.core.SecretsManagerOptions; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -268,6 +270,12 @@ public class KsmSecretService implements VaultSecretService { ? ((Connection) connectable).getParentIdentifier() : ((ConnectionGroup) connectable).getIdentifier(); + // Keep track of all group identifiers seen while recursing up the tree + // in case there's a cycle - if the same identifier is ever seen twice, + // the search is over. + Set observedIdentifiers = new HashSet<>(); + observedIdentifiers.add(parentIdentifier); + Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); while (true) { @@ -284,6 +292,11 @@ public class KsmSecretService implements VaultSecretService { // Otherwise, keep searching up the tree until an appropriate configuration is found parentIdentifier = group.getParentIdentifier(); + + // If the parent is a group that's already been seen, this is a cycle, so there's no + // need to search any further + if (!observedIdentifiers.add(parentIdentifier)) + break; } // If no KSM configuration was ever found, use the default value From c9a2fc249e7ef4993d2cab3831fe07a9dc6a3c19 Mon Sep 17 00:00:00 2001 From: elijahnicpon Date: Mon, 25 Jul 2022 19:03:27 -0400 Subject: [PATCH 039/289] GUACAMOLE-1320: Provide chunked file upload mechanism --- .../src/app/rest/services/tunnelService.js | 127 +++++++++++++----- .../tunnel/InputStreamInterceptingFilter.java | 8 +- 2 files changed, 95 insertions(+), 40 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js index 5f9a21a7c..c5a10d6e4 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js @@ -54,6 +54,15 @@ angular.module('rest').factory('tunnelService', ['$injector', */ var DOWNLOAD_CLEANUP_WAIT = 5000; + /** + * The maximum size a chunk may be during uploadToStream() in bytes. + * + * @private + * @constant + * @type Number + */ + const CHUNK_SIZE = 1024 * 1024 * 4; + /** * Makes a request to the REST API to get the list of all tunnels * associated with in-progress connections, returning a promise that @@ -301,51 +310,99 @@ angular.module('rest').factory('tunnelService', ['$injector', + '/' + encodeURIComponent(sanitizeFilename(file.name)) + '?token=' + encodeURIComponent(authenticationService.getCurrentToken()); - var xhr = new XMLHttpRequest(); + /** + * Creates a chunk of the inputted file to be uploaded. + * + * @param {Number} offset + * The byte at which to begin the chunk. + * + * @return {File} + * The file chunk created by this function. + */ + const createChunk = (offset) => { + var chunkEnd = Math.min(offset + CHUNK_SIZE, file.size); + const chunk = file.slice(offset, chunkEnd); + return chunk; + }; - // Invoke provided callback if upload tracking is supported - if (progressCallback && xhr.upload) { - xhr.upload.addEventListener('progress', function updateProgress(e) { - progressCallback(e.loaded); - }); - } + /** + * POSTs the inputted chunks and recursively calls uploadHandler() + * until the upload is complete. + * + * @param {File} chunk + * The chunk to be uploaded to the stream. + * + * @param {Number} offset + * The byte at which the inputted chunk begins. + */ + const uploadChunk = (chunk, offset) => { + var xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); - // Resolve/reject promise once upload has stopped - xhr.onreadystatechange = function uploadStatusChanged() { + // Invoke provided callback if upload tracking is supported. + if (progressCallback && xhr.upload) { + xhr.upload.addEventListener('progress', function updateProgress(e) { + progressCallback(e.loaded + offset); + }); + }; - // Ignore state changes prior to completion - if (xhr.readyState !== 4) - return; + // Continue to next chunk, resolve, or reject promise as appropriate + // once upload has stopped + xhr.onreadystatechange = function uploadStatusChanged() { - // Resolve if HTTP status code indicates success - if (xhr.status >= 200 && xhr.status < 300) - deferred.resolve(); + // Ignore state changes prior to completion. + if (xhr.readyState !== 4) + return; - // Parse and reject with resulting JSON error - else if (xhr.getResponseHeader('Content-Type') === 'application/json') - deferred.reject(new Error(angular.fromJson(xhr.responseText))); + // Resolve if last chunk or begin next chunk if HTTP status + // code indicates success. + if (xhr.status >= 200 && xhr.status < 300) { + offset += CHUNK_SIZE; - // Warn of lack of permission of a proxy rejects the upload - else if (xhr.status >= 400 && xhr.status < 500) - deferred.reject(new Error({ - 'type' : Error.Type.STREAM_ERROR, - 'statusCode' : Guacamole.Status.Code.CLIENT_FORBIDDEN, - 'message' : 'HTTP ' + xhr.status - })); + if (offset < file.size) + uploadHandler(offset); + else + deferred.resolve(); + } - // Assume internal error for all other cases - else - deferred.reject(new Error({ - 'type' : Error.Type.STREAM_ERROR, - 'statusCode' : Guacamole.Status.Code.INTERNAL_ERROR, - 'message' : 'HTTP ' + xhr.status - })); + // Parse and reject with resulting JSON error + else if (xhr.getResponseHeader('Content-Type') === 'application/json') + deferred.reject(new Error(angular.fromJson(xhr.responseText))); + + // Warn of lack of permission of a proxy rejects the upload + else if (xhr.status >= 400 && xhr.status < 500) + deferred.reject(new Error({ + 'type': Error.Type.STREAM_ERROR, + 'statusCode': Guacamole.Status.Code.CLIENT_FORBIDDEN, + 'message': 'HTTP ' + xhr.status + })); + + // Assume internal error for all other cases + else + deferred.reject(new Error({ + 'type': Error.Type.STREAM_ERROR, + 'statusCode': Guacamole.Status.Code.INTERNAL_ERROR, + 'message': 'HTTP ' + xhr.status + })); + + }; + + // Perform upload + xhr.send(chunk); }; - // Perform upload - xhr.open('POST', url, true); - xhr.send(file); + /** + * Handler for the upload process. + * + * @param {Number} offset + * The byte at which to begin the chunk. + */ + const uploadHandler = (offset) => { + uploadChunk(createChunk(offset), offset); + }; + + uploadHandler(0); return deferred.promise; diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java index f8e033416..6f725bfc1 100644 --- a/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java @@ -94,8 +94,7 @@ public class InputStreamInterceptingFilter /** * Reads the next chunk of data from the InputStream associated with an * intercepted stream, sending that data as a "blob" instruction over the - * GuacamoleTunnel associated with this filter. If the end of the - * InputStream is reached, an "end" instruction will automatically be sent. + * GuacamoleTunnel associated with this filter. * * @param stream * The stream from which the next chunk of data should be read. @@ -112,9 +111,8 @@ public class InputStreamInterceptingFilter // End stream if no more data if (length == -1) { - // Close stream, send end if the stream is still valid - if (closeInterceptedStream(stream)) - sendEnd(stream.getIndex()); + // Close stream + closeInterceptedStream(stream); return; From 003f7e945a8f14982e088aeb3c2b4df506ebd1a8 Mon Sep 17 00:00:00 2001 From: elijahnicpon Date: Wed, 27 Jul 2022 20:26:17 -0400 Subject: [PATCH 040/289] GUACAMOLE-1320: Provide chunked file upload mechanism - add sendEnd() --- .../frontend/src/app/client/types/ManagedFileUpload.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedFileUpload.js b/guacamole/src/main/frontend/src/app/client/types/ManagedFileUpload.js index 56587fcd0..95eef0c61 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedFileUpload.js +++ b/guacamole/src/main/frontend/src/app/client/types/ManagedFileUpload.js @@ -163,17 +163,19 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector' // Upload complete managedFileUpload.progress = file.size; + + // Close the stream + stream.sendEnd(); ManagedFileTransferState.setStreamState(managedFileUpload.transferState, ManagedFileTransferState.StreamState.CLOSED); // Notify of upload completion $rootScope.$broadcast('guacUploadComplete', file.name); - }, // Notify if upload fails requestService.createErrorCallback(function uploadFailed(error) { - + // Use provide status code if the error is coming from the stream if (error.type === Error.Type.STREAM_ERROR) ManagedFileTransferState.setStreamState(managedFileUpload.transferState, @@ -185,11 +187,15 @@ angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector' ManagedFileTransferState.setStreamState(managedFileUpload.transferState, ManagedFileTransferState.StreamState.ERROR, Guacamole.Status.Code.INTERNAL_ERROR); + + // Close the stream + stream.sendEnd(); })); // Ignore all further acks stream.onack = null; + }; From b35b4d509f4f2087cef55b9ddce930df0f87c2f0 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 28 Jul 2022 00:44:34 +0000 Subject: [PATCH 041/289] GUACAMOLE-1372: Modify usage of SAML library to allow signed requests. --- .../acs/AuthenticationSessionManager.java | 23 ++++++++++ .../guacamole/auth/saml/acs/SAMLService.java | 45 ++++++++++++++----- .../auth/saml/conf/ConfigurationService.java | 10 ++++- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSessionManager.java index 198347cda..2e55d2cfc 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/AuthenticationSessionManager.java @@ -126,6 +126,8 @@ public class AuthenticationSessionManager { * call to resume(). If authentication is never resumed, the session will * automatically be cleaned up after it ceases to be valid. * + * This method will automatically generate a new identifier. + * * @param session * The {@link AuthenticationSession} representing the in-progress SAML * authentication attempt. @@ -140,6 +142,27 @@ public class AuthenticationSessionManager { return identifier; } + /** + * Defers the Guacamole side of authentication for the user having the + * given authentication session such that it may be later resumed through a + * call to resume(). If authentication is never resumed, the session will + * automatically be cleaned up after it ceases to be valid. + * + * This method accepts an externally generated ID, which should be a UUID + * or similar unique identifier. + * + * @param session + * The {@link AuthenticationSession} representing the in-progress SAML + * authentication attempt. + * + * @param identifier + * A unique and unpredictable string that may be used to represent the + * given session when calling resume(). + */ + public void defer(AuthenticationSession session, String identifier) { + sessions.put(identifier, session); + } + /** * Shuts down the executor service that periodically removes all invalid * authentication sessions. This must be invoked when the SAML extension is diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java index bd945203e..c3357eefb 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/acs/SAMLService.java @@ -21,7 +21,9 @@ package org.apache.guacamole.auth.saml.acs; import com.google.inject.Inject; import com.google.inject.Singleton; +import com.onelogin.saml2.Auth; import com.onelogin.saml2.authn.AuthnRequest; +import com.onelogin.saml2.authn.AuthnRequestParams; import com.onelogin.saml2.authn.SamlResponse; import com.onelogin.saml2.exception.SettingsException; import com.onelogin.saml2.exception.ValidationError; @@ -29,7 +31,6 @@ import com.onelogin.saml2.settings.Saml2Settings; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import javax.ws.rs.core.UriBuilder; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; import org.apache.guacamole.GuacamoleException; @@ -57,6 +58,12 @@ public class SAMLService { @Inject private AuthenticationSessionManager sessionManager; + /** + * Generator of arbitrary, unique, unpredictable identifiers. + */ + @Inject + private IdentifierGenerator idGenerator; + /** * Creates a new SAML request, beginning the overall authentication flow * that will ultimately result in an asserted user identity if the user is @@ -75,20 +82,31 @@ public class SAMLService { public URI createRequest() throws GuacamoleException { Saml2Settings samlSettings = confService.getSamlSettings(); - AuthnRequest samlReq = new AuthnRequest(samlSettings); - - // Create a new authentication session to represent this attempt while - // it is in progress - AuthenticationSession session = new AuthenticationSession(samlReq.getId(), - confService.getAuthenticationTimeout() * 60000L); // Produce redirect for continuing the authentication process with // the SAML IdP try { - return UriBuilder.fromUri(samlSettings.getIdpSingleSignOnServiceUrl().toURI()) - .queryParam("SAMLRequest", samlReq.getEncodedAuthnRequest()) - .queryParam("RelayState", sessionManager.defer(session)) - .build(); + Auth auth = new Auth(samlSettings, null, null); + + // Generate a unique ID to use for the relay state + String identifier = idGenerator.generateIdentifier(); + + // Create the request URL for the SAML IdP + String requestUrl = auth.login( + identifier, + new AuthnRequestParams(false, false, true), + true); + + // Create a new authentication session to represent this attempt while + // it is in progress, using the request ID that was just issued + AuthenticationSession session = new AuthenticationSession( + auth.getLastRequestId(), + confService.getAuthenticationTimeout() * 60000L); + + // Save the session with the unique relay state ID + sessionManager.defer(session, identifier); + + return new URI(requestUrl); } catch (IOException e) { throw new GuacamoleServerException("SAML authentication request " @@ -99,6 +117,11 @@ public class SAMLService { + "be generated due to an error in the URI syntax: " + e.getMessage()); } + catch (SettingsException e) { + throw new GuacamoleServerException("Error while attempting to sign " + + "request using provided private key / certificate: " + + e.getMessage(), e); + } } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java index 95862bd34..d2a73c46a 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/java/org/apache/guacamole/auth/saml/conf/ConfigurationService.java @@ -492,7 +492,15 @@ public class ConfigurationService { samlSettings.setDebug(getDebug()); samlSettings.setCompressRequest(getCompressRequest()); samlSettings.setCompressResponse(getCompressResponse()); - + + // Request that the SAML library sign everything that it can, if + // both private key and certificate are specified + if (privateKeyFile != null && certificateFile != null) { + samlSettings.setAuthnRequestsSigned(true); + samlSettings.setLogoutRequestSigned(true); + samlSettings.setLogoutResponseSigned(true); + } + return samlSettings; } From a116208a6da0a3d3a6aa92352f5c33f3ac336e73 Mon Sep 17 00:00:00 2001 From: elijahnicpon Date: Thu, 28 Jul 2022 19:17:05 -0400 Subject: [PATCH 042/289] GUACAMOLE-1320: Provide chunked file upload mechanism - update uploadHandler documentation --- .../main/frontend/src/app/rest/services/tunnelService.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js index c5a10d6e4..5cc547b40 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js @@ -393,7 +393,11 @@ angular.module('rest').factory('tunnelService', ['$injector', }; /** - * Handler for the upload process. + * Handles the recursive upload process. Each time it is called, a + * chunk is made with createChunk(), starting at the offset parameter. + * The chunk is then sent by uploadChunk(), which recursively calls + * this handler until the upload process is either completed and the + * promise is resolved, or fails and the promise is rejected. * * @param {Number} offset * The byte at which to begin the chunk. From a4713ca53b8554d320fbfe170ddfb487fd7b20f8 Mon Sep 17 00:00:00 2001 From: Niubbo75 Date: Mon, 8 Aug 2022 14:17:45 +0200 Subject: [PATCH 043/289] GUACAMOLE-1612: Fix/Add italian translations --- .../src/main/resources/translations/it.json | 13 ++ .../src/main/resources/translations/it.json | 113 ++++++++++++++++++ .../src/main/resources/translations/it.json | 18 +++ .../src/main/resources/translations/it.json | 26 ++++ .../src/main/resources/translations/it.json | 43 +++++++ .../src/main/resources/translations/it.json | 14 +++ 6 files changed, 227 insertions(+) create mode 100644 extensions/guacamole-auth-duo/src/main/resources/translations/it.json create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json create mode 100644 extensions/guacamole-auth-quickconnect/src/main/resources/translations/it.json create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json create mode 100644 extensions/guacamole-auth-totp/src/main/resources/translations/it.json create mode 100644 extensions/guacamole-history-recording-storage/src/main/resources/translations/it.json diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/it.json b/extensions/guacamole-auth-duo/src/main/resources/translations/it.json new file mode 100644 index 000000000..a16710c7c --- /dev/null +++ b/extensions/guacamole-auth-duo/src/main/resources/translations/it.json @@ -0,0 +1,13 @@ +{ + + "DATA_SOURCE_DUO" : { + "NAME" : "Duo TFA Backend" + }, + + "LOGIN" : { + "FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "", + "INFO_DUO_VALIDATION_CODE_INCORRECT" : "Codice di convalida Duo errato.", + "INFO_DUO_AUTH_REQUIRED" : "Si prega di autenticarsi con Duo per continuare." + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json new file mode 100644 index 000000000..c4a4e0305 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json @@ -0,0 +1,113 @@ +{ + + "LOGIN" : { + + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", + "ERROR_PASSWORD_SAME" : "La nuova password deve essere diversa dalla password scaduta.", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_NOT_VALID" : "Questo account utente non è attualmente valido.", + "ERROR_NOT_ACCESSIBLE" : "L'accesso a questo account non è attualmente consentito. Per favore riprova più tardi.", + + "INFO_PASSWORD_EXPIRED" : "La tua password è scaduta e deve essere reimpostata. Inserisci una nuova password per continuare.", + + "FIELD_HEADER_NEW_PASSWORD" : "Nuova password", + "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Conferma la nuova password" + + }, + + "CONNECTION_ATTRIBUTES" : { + + "FIELD_HEADER_MAX_CONNECTIONS" : "Numero massimo di connessioni:", + "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Numero massimo di connessioni per utente:", + + "FIELD_HEADER_FAILOVER_ONLY" : "Utilizzare solo per il failover:", + "FIELD_HEADER_WEIGHT" : "Peso della connessione:", + + "FIELD_HEADER_GUACD_HOSTNAME" : "Nome host:", + "FIELD_HEADER_GUACD_ENCRYPTION" : "Crittografia:", + "FIELD_HEADER_GUACD_PORT" : "Porta:", + + "FIELD_OPTION_GUACD_ENCRYPTION_EMPTY" : "", + "FIELD_OPTION_GUACD_ENCRYPTION_NONE" : "Nessuno (non crittografato)", + "FIELD_OPTION_GUACD_ENCRYPTION_SSL" : "SSL / TLS", + + "SECTION_HEADER_CONCURRENCY" : "Limiti di concorrenza", + "SECTION_HEADER_LOAD_BALANCING" : "Load Balancing", + "SECTION_HEADER_GUACD" : "Guacamole Proxy Parameters (guacd)" + + }, + + "CONNECTION_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_ENABLE_SESSION_AFFINITY" : "Abilita affinità di sessione:", + "FIELD_HEADER_MAX_CONNECTIONS" : "Numero massimo di connessioni:", + "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Numero massimo di connessioni per utente:", + + "SECTION_HEADER_CONCURRENCY" : "Limiti di concorrenza (gruppi di bilanciamento)" + + }, + + "DATA_SOURCE_MYSQL" : { + "NAME" : "MySQL" + }, + + "DATA_SOURCE_MYSQL_SHARED" : { + "NAME" : "Connessioni condivise (MySQL)" + }, + + "DATA_SOURCE_POSTGRESQL" : { + "NAME" : "PostgreSQL" + }, + + "DATA_SOURCE_POSTGRESQL_SHARED" : { + "NAME" : "Connessioni condivise (PostgreSQL)" + }, + + "DATA_SOURCE_SQLSERVER" : { + "NAME" : "SQL Server" + }, + + "DATA_SOURCE_SQLSERVER_SHARED" : { + "NAME" : "Connessioni condivise (SQL Server)" + }, + + "HOME" : { + "INFO_SHARED_BY" : "Condiviso da {USERNAME}" + }, + + "PASSWORD_POLICY" : { + + "ERROR_CONTAINS_USERNAME" : "Le password non devono contenere il nome utente", + "ERROR_REQUIRES_DIGIT" : "Le password devono contenere almeno una cifra.", + "ERROR_REQUIRES_MULTIPLE_CASE" : "Le password devono contenere sia caratteri maiuscoli che minuscoli.", + "ERROR_REQUIRES_NON_ALNUM" : "Le password devono contenere almeno un simbolo.", + "ERROR_REUSED" : "Questa password è già stata utilizzata. Si prega di non riutilizzare nessuna delle precedenti {HISTORY_SIZE} {HISTORY_SIZE, plural, one{password} other{passwords}}.", + "ERROR_TOO_SHORT" : "Le password devono essere almeno {LENGTH} {LENGTH, plural, one{carattere} other{caratteri}} di lunghezza.", + "ERROR_TOO_YOUNG" : "La password per questo account è già stata reimpostata. Per favore aspetta almeno {WAIT} altri {WAIT, plural, one{giorno} other{giorni}} prima di modificare nuovamente la password." + + }, + + "USER_ATTRIBUTES" : { + + "FIELD_HEADER_DISABLED" : "Accesso disabilitato:", + "FIELD_HEADER_EXPIRED" : "Password scaduta:", + "FIELD_HEADER_ACCESS_WINDOW_END" : "Non consentire l'accesso dopo:", + "FIELD_HEADER_ACCESS_WINDOW_START" : "Consenti l'accesso dopo:", + "FIELD_HEADER_TIMEZONE" : "Fuso orario utente:", + "FIELD_HEADER_VALID_FROM" : "Abilita account dopo:", + "FIELD_HEADER_VALID_UNTIL" : "Disattiva account dopo:", + + "SECTION_HEADER_RESTRICTIONS" : "Restrizioni dell'account", + "SECTION_HEADER_PROFILE" : "Profilo" + + }, + + "USER_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_DISABLED" : "Disabilitato:", + + "SECTION_HEADER_RESTRICTIONS" : "Restrizioni di gruppo" + + } + +} diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/translations/it.json b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/it.json new file mode 100644 index 000000000..bb99a7730 --- /dev/null +++ b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/it.json @@ -0,0 +1,18 @@ +{ + + "DATA_SOURCE_QUICKCONNECT" : { + "NAME" : "QuickConnect" + }, + + "QUICKCONNECT" : { + "ACTION_CONNECT" : "Collegare", + + "ERROR_INVALID_URI" : "URI specificato non valido", + "ERROR_NO_HOST" : "Nessun host specificato", + "ERROR_NO_PROTOCOL" : "Nessun protocollo specificato", + "ERROR_NOT_ABSOLUTE_URI" : "L'URI non è assoluto", + + "FIELD_PLACEHOLDER_URI" : "Immettere l'URI di connessione" + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json new file mode 100644 index 000000000..246029367 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json @@ -0,0 +1,26 @@ +{ + + "DATA_SOURCE_CAS" : { + "NAME" : "CAS SSO Backend" + }, + + "DATA_SOURCE_OPENID" : { + "NAME" : "OpenID SSO Backend" + }, + + "DATA_SOURCE_SAML" : { + "NAME" : "SAML SSO Backend" + }, + + "LOGIN" : { + "FIELD_HEADER_ID_TOKEN" : "", + "FIELD_HEADER_STATE" : "", + "FIELD_HEADER_TICKET" : "", + "INFO_IDP_REDIRECT_PENDING" : "Attendi, reindirizzamento al provider di identità...", + "NAME_IDP_CAS" : "CAS", + "NAME_IDP_OPENID" : "OpenID", + "NAME_IDP_SAML" : "SAML", + "SECTION_HEADER_SSO_OPTIONS" : "Accedi con:" + } + +} diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/it.json b/extensions/guacamole-auth-totp/src/main/resources/translations/it.json new file mode 100644 index 000000000..97b30f469 --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/resources/translations/it.json @@ -0,0 +1,43 @@ +{ + + "DATA_SOURCE_TOTP" : { + "NAME" : "TOTP TFA Backend" + }, + + "LOGIN" : { + "FIELD_HEADER_GUAC_TOTP" : "" + }, + + "TOTP" : { + + "ACTION_HIDE_DETAILS" : "Nascondere", + "ACTION_SHOW_DETAILS" : "Mostrare", + + "FIELD_HEADER_ALGORITHM" : "Algoritmo:", + "FIELD_HEADER_DIGITS" : "Cifre:", + "FIELD_HEADER_INTERVAL" : "Intervallo:", + "FIELD_HEADER_SECRET_KEY" : "Chiave segreta:", + + "FIELD_PLACEHOLDER_CODE" : "Codice di Autenticazione", + + "INFO_CODE_REQUIRED" : "Inserisci il tuo codice di autenticazione per verificare la tua identità.", + "INFO_ENROLL_REQUIRED" : "L'autenticazione a più fattori è stata abilitata sul tuo account.", + "INFO_VERIFICATION_FAILED" : "Verifica fallita. Per favore riprova.", + + "HELP_ENROLL_BARCODE" : "Per completare il processo di registrazione, scansiona il codice a barre qui sotto con l'app di autenticazione a due fattori sul tuo telefono o dispositivo.", + "HELP_ENROLL_VERIFY" : "Dopo aver scansionato il codice a barre, inserire il codice di autenticazione a {DIGITS}-cifre visualizzato per verificare che la registrazione sia andata a buon fine.", + + "SECTION_HEADER_DETAILS" : "Dettagli:" + + }, + + "USER_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Chiave segreta generata:", + "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Dispositivo di autenticazione confermato:", + + "SECTION_HEADER_TOTP_ENROLLMENT_STATUS" : "Stato di iscrizione TOTP" + + } + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/translations/it.json b/extensions/guacamole-history-recording-storage/src/main/resources/translations/it.json new file mode 100644 index 000000000..bec1fc261 --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/resources/translations/it.json @@ -0,0 +1,14 @@ +{ + + "DATA_SOURCE_RECORDING_STORAGE" : { + "NAME" : "Archiviazione della registrazione della sessione" + }, + + "RECORDING_STORAGE" : { + "INFO_GUACAMOLE_SESSION_RECORDING" : "Registrazione grafica della sessione desktop remoto", + "INFO_SERVER_LOG" : "Server/system log", + "INFO_TYPESCRIPT" : "Registrazione del testo della sessione del terminale", + "INFO_TYPESCRIPT_TIMING" : "Informazioni sull'orario per la registrazione del testo della sessione del terminale" + } + +} From 4fca3127fbd3c75377aed908f0c9c34a56d63a2d Mon Sep 17 00:00:00 2001 From: Jean-Benoit PAUX <9682558+jbpaux@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:05:51 +0200 Subject: [PATCH 044/289] GUACAMOLE-1572: match PostgreSQL environment variables with guacamole.properties variables --- guacamole-docker/README.md | 30 +++++------ guacamole-docker/bin/start.sh | 94 +++++++++++++++++------------------ 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/guacamole-docker/README.md b/guacamole-docker/README.md index 0f6200f89..e800345eb 100644 --- a/guacamole-docker/README.md +++ b/guacamole-docker/README.md @@ -55,31 +55,31 @@ Deploying Guacamole with PostgreSQL authentication -------------------------------------------------- docker run --name some-guacamole --link some-guacd:guacd \ - --link some-postgres:postgres \ - -e POSTGRES_DATABASE=guacamole_db \ - -e POSTGRES_USER=guacamole_user \ - -e POSTGRES_PASSWORD=some_password \ - -e POSTGRES_DATABASE_FILE=/run/secrets/ \ - -e POSTGRES_USER_FILE=/run/secrets/ \ - -e POSTGRES_PASSWORD_FILE=/run/secrets/ \ + --link some-postgresql:postgresql \ + -e POSTGRESQL_DATABASE=guacamole_db \ + -e POSTGRESQL_USER=guacamole_user \ + -e POSTGRESQL_PASSWORD=some_password \ + -e POSTGRESQL_DATABASE_FILE=/run/secrets/ \ + -e POSTGRESQL_USER_FILE=/run/secrets/ \ + -e POSTGRESQL_PASSWORD_FILE=/run/secrets/ \ -d -p 8080:8080 guacamole/guacamole Linking Guacamole to PostgreSQL requires three environment variables. If any of these environment variables are omitted, you will receive an error message, and the image will stop: -1. `POSTGRES_DATABASE` - The name of the database to use for Guacamole +1. `POSTGRESQL_DATABASE` - The name of the database to use for Guacamole authentication. -2. `POSTGRES_USER` - The user that Guacamole will use to connect to PostgreSQL. -3. `POSTGRES_PASSWORD` - The password that Guacamole will provide when - connecting to PostgreSQL as `POSTGRES_USER`. -4. `POSTGRES_DATABASE_FILE` - The path of the docker secret containing the name +2. `POSTGRESQL_USER` - The user that Guacamole will use to connect to PostgreSQL. +3. `POSTGRESQL_PASSWORD` - The password that Guacamole will provide when + connecting to PostgreSQL as `POSTGRESQL_USER`. +4. `POSTGRESQL_DATABASE_FILE` - The path of the docker secret containing the name of database to use for Guacamole authentication. -5. `POSTGRES_USER_FILE` - The path of the docker secret containing the name of +5. `POSTGRESQL_USER_FILE` - The path of the docker secret containing the name of the user that Guacamole will use to connect to PostgreSQL. -6. `POSTGRES_PASSWORD_FILE` - The path of the docker secret containing the +6. `POSTGRESQL_PASSWORD_FILE` - The path of the docker secret containing the password that Guacamole will provide when connecting to PostgreSQL as - `POSTGRES_USER. + `POSTGRESQL_USER. ### Initializing the PostgreSQL database diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh index c9d205b58..4557576e0 100755 --- a/guacamole-docker/bin/start.sh +++ b/guacamole-docker/bin/start.sh @@ -242,7 +242,7 @@ END } # Print error message regarding missing required variables for PostgreSQL authentication -postgres_missing_vars() { +postgresql_missing_vars() { cat < Date: Mon, 11 Apr 2022 15:06:59 +0200 Subject: [PATCH 045/289] GUACAMOLE-1572: Update init script to match postgresql too --- guacamole-docker/README.md | 2 +- guacamole-docker/bin/initdb.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/guacamole-docker/README.md b/guacamole-docker/README.md index e800345eb..78631e852 100644 --- a/guacamole-docker/README.md +++ b/guacamole-docker/README.md @@ -91,7 +91,7 @@ To generate a SQL script which can be used to initialize a fresh PostgreSQL database [as documented in the Guacamole manual](http://guacamole.apache.org/doc/gug/jdbc-auth.html#jdbc-auth-postgresql): - docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgres > initdb.sql + docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --postgresql > initdb.sql Alternatively, you can use the SQL scripts included with the guacamole-auth-jdbc extension from diff --git a/guacamole-docker/bin/initdb.sh b/guacamole-docker/bin/initdb.sh index 6031189c9..aa92b66bb 100755 --- a/guacamole-docker/bin/initdb.sh +++ b/guacamole-docker/bin/initdb.sh @@ -26,7 +26,7 @@ ## ## @param DATABASE ## The database to generate the SQL script for. This may be either -## "--postgres", for PostgreSQL, "--mysql" for MySQL, or "--sqlserver" for Microsoft SQL Server. +## "--postgresql", for PostgreSQL, "--mysql" for MySQL, or "--sqlserver" for Microsoft SQL Server. ## DATABASE="$1" @@ -37,7 +37,7 @@ DATABASE="$1" ## incorrect_usage() { cat < Date: Wed, 27 Apr 2022 15:13:41 +0200 Subject: [PATCH 046/289] GUACAMOLE-1572: Revert postgres link name change --- guacamole-docker/README.md | 2 +- guacamole-docker/bin/start.sh | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/guacamole-docker/README.md b/guacamole-docker/README.md index 78631e852..a6e7e1670 100644 --- a/guacamole-docker/README.md +++ b/guacamole-docker/README.md @@ -55,7 +55,7 @@ Deploying Guacamole with PostgreSQL authentication -------------------------------------------------- docker run --name some-guacamole --link some-guacd:guacd \ - --link some-postgresql:postgresql \ + --link some-postgres:postgres \ -e POSTGRESQL_DATABASE=guacamole_db \ -e POSTGRESQL_USER=guacamole_user \ -e POSTGRESQL_PASSWORD=some_password \ diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh index 4557576e0..f57e5feba 100755 --- a/guacamole-docker/bin/start.sh +++ b/guacamole-docker/bin/start.sh @@ -273,9 +273,9 @@ END associate_postgresql() { # Use linked container if specified - if [ -n "$POSTGRESQL_NAME" ]; then - POSTGRESQL_HOSTNAME="$POSTGRESQL_PORT_5432_TCP_ADDR" - POSTGRESQL_PORT="$POSTGRESQL_PORT_5432_TCP_PORT" + if [ -n "$POSTGRES_NAME" ]; then + POSTGRESQL_HOSTNAME="$POSTGRES_PORT_5432_TCP_ADDR" + POSTGRESQL_PORT="$POSTGRES_PORT_5432_TCP_PORT" fi # Use default port if none specified @@ -284,11 +284,11 @@ associate_postgresql() { # Verify required connection information is present if [ -z "$POSTGRESQL_HOSTNAME" -o -z "$POSTGRESQL_PORT" ]; then cat < Date: Wed, 27 Apr 2022 15:52:50 +0200 Subject: [PATCH 047/289] GUACAMOLE-1572: Add compatibility mechanism for POSTGRES variables --- guacamole-docker/bin/start.sh | 71 +++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh index f57e5feba..ef55d5737 100755 --- a/guacamole-docker/bin/start.sh +++ b/guacamole-docker/bin/start.sh @@ -263,6 +263,63 @@ END exit 1; } +## Provide backward compatibility on POSTGRES_* environment variables +## In case of new deployment, please use POSTGRESQL_* equivalent variables. +if [ -n "$POSTGRES_HOSTNAME" ]; then + POSTGRESQL_HOSTNAME=$POSTGRES_HOSTNAME; +fi +if [ -n "$POSTGRES_PORT" ]; then + POSTGRESQL_PORT=$POSTGRES_PORT; +fi +if [ -n "$POSTGRES_DATABASE" ]; then + POSTGRESQL_DATABASE=$POSTGRES_DATABASE; +fi +if [ -n "$POSTGRES_DATABASE_FILE" ]; then + POSTGRESQL_DATABASE=$POSTGRES_DATABASE_FILE; +fi +if [ -n "$POSTGRES_USER_FILE" ]; then + POSTGRESQL_USER_FILE=$POSTGRES_USER_FILE; +fi +if [ -n "$POSTGRES_USER" ]; then + POSTGRESQL_USER=$POSTGRES_USER; +fi +if [ -n "$POSTGRES_PASSWORD_FILE" ]; then + POSTGRESQL_PASSWORD_FILE=$POSTGRES_PASSWORD_FILE; +fi +if [ -n "$POSTGRES_PASSWORD" ]; then + POSTGRESQL_PASSWORD=$POSTGRES_PASSWORD; +fi +if [ -n "$POSTGRES_ABSOLUTE_MAX_CONNECTIONS" ]; then + POSTGRESQL_ABSOLUTE_MAX_CONNECTIONS=$POSTGRES_ABSOLUTE_MAX_CONNECTIONS; +fi +if [ -n "$POSTGRES_DEFAULT_MAX_CONNECTIONS" ]; then + POSTGRESQL_DEFAULT_MAX_CONNECTIONS=$POSTGRES_DEFAULT_MAX_CONNECTIONS; +fi +if [ -n "$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS" ]; then + POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS=$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS; +fi +if [ -n "$POSTGRES_DEFAULT_MAX_CONNECTIONS_PER_USER" ]; then + POSTGRESQL_DEFAULT_MAX_CONNECTIONS_PER_USER=$POSTGRES_DEFAULT_MAX_CONNECTIONS_PER_USER; +fi +if [ -n "$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER" ]; then + POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER=$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER; +fi +if [ -n "$POSTGRES_DEFAULT_STATEMENT_TIMEOUT" ]; then + POSTGRESQL_DEFAULT_STATEMENT_TIMEOUT=$POSTGRES_DEFAULT_STATEMENT_TIMEOUT; +fi +if [ -n "$POSTGRES_USER_REQUIRED" ]; then + POSTGRESQL_USER_REQUIRED=$POSTGRES_USER_REQUIRED; +fi +if [ -n "$POSTGRES_SOCKET_TIMEOUT" ]; then + POSTGRESQL_SOCKET_TIMEOUT=$POSTGRES_SOCKET_TIMEOUT; +fi +if [ -n "$POSTGRES_SSL_KEY_PASSWORD_FILE" ]; then + POSTGRESQL_SSL_KEY_PASSWORD_FILE=$POSTGRES_SSL_KEY_PASSWORD_FILE; +fi +if [ -n "$POSTGRES_SSL_KEY_PASSWORD" ]; then + POSTGRESQL_SSL_KEY_PASSWORD=$POSTGRES_SSL_KEY_PASSWORD; +fi + ## ## Adds properties to guacamole.properties which select the PostgreSQL ## authentication provider, and configure it to connect to the linked @@ -294,14 +351,14 @@ If using a PostgreSQL database, you must either: connection to your database using the following environment variables: POSTGRESQL_HOSTNAME The hostname or IP address of the PostgreSQL server. If - not using a PostgreSQL Docker container and - corresponding link, this environment variable is - *REQUIRED*. + not using a PostgreSQL Docker container and + corresponding link, this environment variable is + *REQUIRED*. POSTGRESQL_PORT The port on which the PostgreSQL server is listening for - TCP connections. This environment variable is option. If - omitted, the standard PostgreSQL port of 5432 will be - used. + TCP connections. This environment variable is option. If + omitted, the standard PostgreSQL port of 5432 will be + used. END exit 1; fi @@ -430,7 +487,7 @@ set the path of the corresponding secrets in the following three variables: the user that Guacamole will use to connect to SQLServer. SQLSERVER_PASSWORD_FILE The path of the docker secret containing the - password that Guacamole will provide when connecting to + password that Guacamole will provide when connecting to SQLServer as SQLSERVER_USER. END From 89b3b138b6d728ee904eaeaf5c0cd1e317c34c69 Mon Sep 17 00:00:00 2001 From: jbpaux <9682558+jbpaux@users.noreply.github.com> Date: Tue, 9 Aug 2022 15:27:10 +0200 Subject: [PATCH 048/289] GUACAMOLE-1572: Make POSTGRESQL env variable backward comp dynamic --- guacamole-docker/bin/start.sh | 76 ++++++++++------------------------- 1 file changed, 21 insertions(+), 55 deletions(-) diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh index ef55d5737..ff0762d04 100755 --- a/guacamole-docker/bin/start.sh +++ b/guacamole-docker/bin/start.sh @@ -1,4 +1,4 @@ -#!/bin/sh -e +#!/bin/bash -e # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file @@ -265,60 +265,26 @@ END ## Provide backward compatibility on POSTGRES_* environment variables ## In case of new deployment, please use POSTGRESQL_* equivalent variables. -if [ -n "$POSTGRES_HOSTNAME" ]; then - POSTGRESQL_HOSTNAME=$POSTGRES_HOSTNAME; -fi -if [ -n "$POSTGRES_PORT" ]; then - POSTGRESQL_PORT=$POSTGRES_PORT; -fi -if [ -n "$POSTGRES_DATABASE" ]; then - POSTGRESQL_DATABASE=$POSTGRES_DATABASE; -fi -if [ -n "$POSTGRES_DATABASE_FILE" ]; then - POSTGRESQL_DATABASE=$POSTGRES_DATABASE_FILE; -fi -if [ -n "$POSTGRES_USER_FILE" ]; then - POSTGRESQL_USER_FILE=$POSTGRES_USER_FILE; -fi -if [ -n "$POSTGRES_USER" ]; then - POSTGRESQL_USER=$POSTGRES_USER; -fi -if [ -n "$POSTGRES_PASSWORD_FILE" ]; then - POSTGRESQL_PASSWORD_FILE=$POSTGRES_PASSWORD_FILE; -fi -if [ -n "$POSTGRES_PASSWORD" ]; then - POSTGRESQL_PASSWORD=$POSTGRES_PASSWORD; -fi -if [ -n "$POSTGRES_ABSOLUTE_MAX_CONNECTIONS" ]; then - POSTGRESQL_ABSOLUTE_MAX_CONNECTIONS=$POSTGRES_ABSOLUTE_MAX_CONNECTIONS; -fi -if [ -n "$POSTGRES_DEFAULT_MAX_CONNECTIONS" ]; then - POSTGRESQL_DEFAULT_MAX_CONNECTIONS=$POSTGRES_DEFAULT_MAX_CONNECTIONS; -fi -if [ -n "$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS" ]; then - POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS=$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS; -fi -if [ -n "$POSTGRES_DEFAULT_MAX_CONNECTIONS_PER_USER" ]; then - POSTGRESQL_DEFAULT_MAX_CONNECTIONS_PER_USER=$POSTGRES_DEFAULT_MAX_CONNECTIONS_PER_USER; -fi -if [ -n "$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER" ]; then - POSTGRESQL_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER=$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER; -fi -if [ -n "$POSTGRES_DEFAULT_STATEMENT_TIMEOUT" ]; then - POSTGRESQL_DEFAULT_STATEMENT_TIMEOUT=$POSTGRES_DEFAULT_STATEMENT_TIMEOUT; -fi -if [ -n "$POSTGRES_USER_REQUIRED" ]; then - POSTGRESQL_USER_REQUIRED=$POSTGRES_USER_REQUIRED; -fi -if [ -n "$POSTGRES_SOCKET_TIMEOUT" ]; then - POSTGRESQL_SOCKET_TIMEOUT=$POSTGRES_SOCKET_TIMEOUT; -fi -if [ -n "$POSTGRES_SSL_KEY_PASSWORD_FILE" ]; then - POSTGRESQL_SSL_KEY_PASSWORD_FILE=$POSTGRES_SSL_KEY_PASSWORD_FILE; -fi -if [ -n "$POSTGRES_SSL_KEY_PASSWORD" ]; then - POSTGRESQL_SSL_KEY_PASSWORD=$POSTGRES_SSL_KEY_PASSWORD; -fi +for VAR_BASE in \ + HOSTNAME PORT \ + DATABASE USER PASSWORD \ + DATABASE_FILE USER_FILE PASSWORD_FILE \ + ABSOLUTE_MAX_CONNECTIONS DEFAULT_MAX_CONNECTIONS \ + DEFAULT_MAX_GROUP_CONNECTIONS DEFAULT_MAX_CONNECTIONS_PER_USER \ + DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER \ + DEFAULT_STATEMENT_TIMEOUT SOCKET_TIMEOUT \ + USER_REQUIRED \ + SSL_KEY_PASSWORD_FILE SSL_KEY_PASSWORD; do + + OLD_VAR="POSTGRES_$VAR_BASE" + NEW_VAR="POSTGRESQL_$VAR_BASE" + + if [ -n "${!OLD_VAR}" ]; then + printf -v "$NEW_VAR" "%s" "${!OLD_VAR}" + echo "WARNING: ${OLD_VAR} detected, please use ${NEW_VAR} for further deployments." + fi + +done ## ## Adds properties to guacamole.properties which select the PostgreSQL From f3d43c1f050d697cebec90ea4da9f8638b930d82 Mon Sep 17 00:00:00 2001 From: jbpaux <9682558+jbpaux@users.noreply.github.com> Date: Tue, 9 Aug 2022 19:03:41 +0200 Subject: [PATCH 049/289] GUACAMOLE-1572: Add space indentation on error message --- guacamole-docker/bin/start.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh index ff0762d04..d7cab4264 100755 --- a/guacamole-docker/bin/start.sh +++ b/guacamole-docker/bin/start.sh @@ -252,13 +252,13 @@ to the environment variable, and setting the value to the path of the corresponding secret: POSTGRESQL_USER The user to authenticate as when connecting to - PostgreSQL. + PostgreSQL. POSTGRESQL_PASSWORD The password to use when authenticating with PostgreSQL - as POSTGRESQL_USER. + as POSTGRESQL_USER. POSTGRESQL_DATABASE The name of the PostgreSQL database to use for Guacamole - authentication. + authentication. END exit 1; } From aa06c81f2985a387c80ffda19991cf6fb8ea27ac Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 8 Aug 2022 23:10:41 +0000 Subject: [PATCH 050/289] GUACAMOLE-1661: Add domain search support for KSM vault extension. --- .../guacamole/vault/ksm/secret/KsmClient.java | 134 +++++++++++++++++- .../vault/ksm/secret/KsmSecretService.java | 12 ++ 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index 5572ef71d..c854fbd3b 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -34,6 +34,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -45,6 +46,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; +import org.apache.guacamole.vault.secret.WindowsUsername; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,6 +66,12 @@ public class KsmClient { */ private static final Logger logger = LoggerFactory.getLogger(KsmClient.class); + /** + * Service for retrieving configuration information. + */ + @Inject + private KsmConfigurationService confService; + /** * Service for retrieving data from records. */ @@ -179,6 +188,30 @@ public class KsmClient { */ private final Set cachedAmbiguousUsernames = new HashSet<>(); + /** + * All records retrieved from Keeper Secrets Manager, where each key is the + * domain of the corresponding record. The domain of a record is + * determined by {@link Login} fields, thus a record may be associated with + * multiple domains. If a record is associated with multiple domains, there + * will be multiple references to that record within this Map. The contents + * of this Map are automatically updated if {@link #validateCache()} + * refreshes the cache. This Map must not be accessed without + * {@link #cacheLock} acquired appropriately. Before using a value from + * this Map, {@link #cachedAmbiguousDomains} must first be checked to + * verify that there is indeed only one record associated with that domain. + */ + private final Map cachedRecordsByDomain = new HashMap<>(); + + /** + * The set of all domains that are associated with multiple records, and + * thus cannot uniquely identify a record. The contents of this Set are + * automatically updated if {@link #validateCache()} refreshes the cache. + * This Set must not be accessed without {@link #cacheLock} acquired + * appropriately. This Set must be checked before using a value retrieved + * from {@link #cachedRecordsByDomain}. + */ + private final Set cachedAmbiguousDomains = new HashSet<>(); + /** * Create a new KSM client based around the provided KSM configuration. * @@ -239,9 +272,17 @@ public class KsmClient { cachedAmbiguousUsernames.clear(); cachedRecordsByUsername.clear(); - // Store all records, sorting each into host-based and login-based - // buckets - records.forEach(record -> { + // Clear cache of domain-based records + cachedAmbiguousDomains.clear(); + cachedRecordsByDomain.clear(); + + // Store all records, sorting each into host-based, login-based, + // and domain-based buckets + Iterator recordIterator = records.iterator(); + while(recordIterator.hasNext()) { + + // Go through records one at a time + KeeperRecord record = recordIterator.next(); // Store based on UID ... cachedRecordsByUid.put(record.getRecordUid(), record); @@ -250,13 +291,38 @@ public class KsmClient { String hostname = recordService.getHostname(record); addRecordForHost(record, hostname); + // ... and domain + String domain = recordService.getDomain(record); + addRecordForDomain(record, domain); + + // Fetch the username + String username = recordService.getUsername(record); + + // If domains should be split out from usernames + if (username != null && confService.getSplitWindowsUsernames()) { + + // Attempt to split the domain of the username + WindowsUsername usernameAndDomain = ( + WindowsUsername.splitWindowsUsernameFromDomain(username)); + + if (usernameAndDomain.hasDomain()) { + + // Update the username if a domain has been stripped off + username = usernameAndDomain.getUsername(); + + // Use the username-split domain if not already set explicitly + if (domain == null) + addRecordForDomain(record, usernameAndDomain.getDomain()); + } + } + // Store based on username ONLY if no hostname (will otherwise // result in ambiguous entries for servers tied to identical // accounts) if (hostname == null) - addRecordForLogin(record, recordService.getUsername(record)); + addRecordForLogin(record, username); - }); + } // Cache has been refreshed this.cacheTimestamp = System.currentTimeMillis(); @@ -268,6 +334,30 @@ public class KsmClient { } + /** + * Associates the given record with the given domain. The domain may be + * null. Both {@link #cachedRecordsByDomain} and {@link #cachedAmbiguousDomains} + * are updated appropriately. The write lock of {@link #cacheLock} must + * already be acquired before invoking this function. + * + * @param record + * The record to associate with the domains in the given field. + * + * @param domain + * The domain that the given record should be associated with. + * This may be null. + */ + private void addRecordForDomain(KeeperRecord record, String domain) { + + if (domain == null) + return; + + KeeperRecord existing = cachedRecordsByDomain.putIfAbsent(domain, record); + if (existing != null && record != existing) + cachedAmbiguousDomains.add(domain); + + } + /** * Associates the given record with the given hostname. The hostname may be * null. Both {@link #cachedRecordsByHost} and {@link #cachedAmbiguousHosts} @@ -432,6 +522,40 @@ public class KsmClient { } } + /** + * Returns the record associated with the given domain. If no such record + * exists, or there are multiple such records, null is returned. + * + * @param domain + * The domain of the record to return. + * + * @return + * The record associated with the given domain, or null if there is + * no such record or multiple such records. + * + * @throws GuacamoleException + * If an error occurs that prevents the record from being retrieved. + */ + public KeeperRecord getRecordByDomain(String domain) throws GuacamoleException { + validateCache(); + cacheLock.readLock().lock(); + try { + + if (cachedAmbiguousDomains.contains(domain)) { + logger.debug("The domain \"{}\" is referenced by multiple " + + "Keeper records and cannot be used to locate " + + "individual secrets.", domain); + return null; + } + + return cachedRecordsByDomain.get(domain); + + } + finally { + cacheLock.readLock().unlock(); + } + } + /** * Returns the value of the secret stored within Keeper Secrets Manager and * represented by the given Keeper notation. Keeper notation locates the diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index d8168dc8f..604ae2865 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -344,6 +344,18 @@ public class KsmSecretService implements VaultSecretService { addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", ksm.getRecordByLogin(filter.filter(gatewayUsername))); + // Retrieve and define domain tokens, if any + String domain = parameters.get("domain"); + if (domain != null && !domain.isEmpty()) + addRecordTokens(tokens, "KEEPER_DOMAIN_", + ksm.getRecordByDomain(filter.filter(domain))); + + // Retrieve and define gateway domain tokens, if any + String gatewayDomain = parameters.get("gateway-domain"); + if (gatewayDomain != null && !gatewayDomain.isEmpty()) + addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_", + ksm.getRecordByDomain(filter.filter(gatewayDomain))); + } return tokens; From 593cfaaffede48f09f72f5b72f9455ef3e56d477 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 10 Aug 2022 23:49:59 +0000 Subject: [PATCH 051/289] GUACAMOLE-1661: Match by both user and domain when using KEEPER_USER_ tokens. --- .../vault/conf/VaultConfigurationService.java | 17 +++ .../ksm/conf/KsmConfigurationService.java | 18 +++ .../guacamole/vault/ksm/secret/KsmClient.java | 121 +++++++++------- .../vault/ksm/secret/KsmSecretService.java | 63 ++++++--- .../vault/ksm/secret/UserDomain.java | 129 ++++++++++++++++++ 5 files changed, 278 insertions(+), 70 deletions(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserDomain.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java index 490da6816..3a3189ba5 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultConfigurationService.java @@ -206,4 +206,21 @@ public abstract class VaultConfigurationService { */ public abstract boolean getSplitWindowsUsernames() throws GuacamoleException; + /** + * Return whether domains should be considered when matching user records + * that are fetched from the vault. + * + * If set to true, the username and domain must both match when matching + * records from the vault. If false, only the username will be considered. + * + * @return + * true if both the username and domain should be considered when + * matching user records from the vault. + * + * @throws GuacamoleException + * If the value specified within guacamole.properties cannot be + * parsed. + */ + public abstract boolean getMatchUserRecordsByDomain() throws GuacamoleException; + } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java index 3ef02b8b9..c9a9536ea 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java @@ -96,6 +96,19 @@ public class KsmConfigurationService extends VaultConfigurationService { } }; + /** + * Whether domains should be considered when matching login records in the KSM vault. + * If true, both the domain and username must match for a record to match when using + * tokens like "KEEPER_USER_*". If false, only the username must match. + */ + private static final BooleanGuacamoleProperty MATCH_USER_DOMAINS = new BooleanGuacamoleProperty() { + + @Override + public String getName() { + return "ksm-match-domains-for-users"; + } + }; + /** * Creates a new KsmConfigurationService which reads the configuration * from "ksm-token-mapping.yml" and properties from @@ -130,6 +143,11 @@ public class KsmConfigurationService extends VaultConfigurationService { return environment.getProperty(STRIP_WINDOWS_DOMAINS, false); } + @Override + public boolean getMatchUserRecordsByDomain() throws GuacamoleException { + return environment.getProperty(MATCH_USER_DOMAINS, false); + } + /** * Return the globally-defined base-64-encoded JSON KSM configuration blob diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index c854fbd3b..34f350234 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -46,6 +46,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.secret.WindowsUsername; import org.slf4j.Logger; @@ -166,27 +167,27 @@ public class KsmClient { /** * All records retrieved from Keeper Secrets Manager, where each key is the - * username of the corresponding record. The username of a record is - * determined by {@link Login} fields, thus a record may be associated with - * multiple users. If a record is associated with multiple users, there - * will be multiple references to that record within this Map. The contents - * of this Map are automatically updated if {@link #validateCache()} - * refreshes the cache. This Map must not be accessed without - * {@link #cacheLock} acquired appropriately. Before using a value from - * this Map, {@link #cachedAmbiguousUsernames} must first be checked to + * username/domain of the corresponding record. The username of a record is + * determined by {@link Login} and "domain" fields, thus a record may be + * associated with multiple users. If a record is associated with multiple + * users, there will be multiple references to that record within this Map. + * The contents of this Map are automatically updated if + * {@link #validateCache()} refreshes the cache. This Map must not be accessed + * without {@link #cacheLock} acquired appropriately. Before using a value from + * this Map, {@link #cachedAmbiguousUsers} must first be checked to * verify that there is indeed only one record associated with that user. */ - private final Map cachedRecordsByUsername = new HashMap<>(); + private final Map cachedRecordsByUser = new HashMap<>(); /** - * The set of all usernames that are associated with multiple records, and - * thus cannot uniquely identify a record. The contents of this Set are - * automatically updated if {@link #validateCache()} refreshes the cache. - * This Set must not be accessed without {@link #cacheLock} acquired - * appropriately.This Set must be checked before using a value retrieved - * from {@link #cachedRecordsByUsername}. + * The set of all username/domain combos that are associated with multiple + * records, and thus cannot uniquely identify a record. The contents of + * this Set are automatically updated if {@link #validateCache()} refreshes + * the cache. This Set must not be accessed without {@link #cacheLock} + * acquired appropriately. This Set must be checked before using a value + * retrieved from {@link #cachedRecordsByUser}. */ - private final Set cachedAmbiguousUsernames = new HashSet<>(); + private final Set cachedAmbiguousUsers = new HashSet<>(); /** * All records retrieved from Keeper Secrets Manager, where each key is the @@ -269,8 +270,8 @@ public class KsmClient { cachedRecordsByHost.clear(); // Clear cache of login-based records - cachedAmbiguousUsernames.clear(); - cachedRecordsByUsername.clear(); + cachedAmbiguousUsers.clear(); + cachedRecordsByUser.clear(); // Clear cache of domain-based records cachedAmbiguousDomains.clear(); @@ -295,32 +296,32 @@ public class KsmClient { String domain = recordService.getDomain(record); addRecordForDomain(record, domain); - // Fetch the username + // Get the username off of the record String username = recordService.getUsername(record); - // If domains should be split out from usernames - if (username != null && confService.getSplitWindowsUsernames()) { + // If we have a username, and there isn't already a domain explicitly defined + if (username != null && domain == null + && confService.getSplitWindowsUsernames()) { - // Attempt to split the domain of the username + // Attempt to split out the domain of the username WindowsUsername usernameAndDomain = ( WindowsUsername.splitWindowsUsernameFromDomain(username)); - if (usernameAndDomain.hasDomain()) { + // Use the username-split domain if not already set explicitly + if (usernameAndDomain.hasDomain()) + domain = usernameAndDomain.getDomain(); + addRecordForDomain(record, domain); - // Update the username if a domain has been stripped off - username = usernameAndDomain.getUsername(); - - // Use the username-split domain if not already set explicitly - if (domain == null) - addRecordForDomain(record, usernameAndDomain.getDomain()); - } } - // Store based on username ONLY if no hostname (will otherwise - // result in ambiguous entries for servers tied to identical - // accounts) - if (hostname == null) - addRecordForLogin(record, username); + // If domain matching is not enabled for user records, + // explicitly set all domains to null to allow matching + // on username only + if (!confService.getMatchUserRecordsByDomain()) + domain = null; + + // Store the login by username and domain + addRecordForLogin(record, username, domain); } @@ -383,27 +384,34 @@ public class KsmClient { } /** - * Associates the given record with the given username. The given username - * may be null. Both {@link #cachedRecordsByUsername} and - * {@link #cachedAmbiguousUsernames} are updated appropriately. The write + * Associates the given record with the given user, and optional domain. + * The given username or domain may be null. Both {@link #cachedRecordsByUser} + * and {@link #cachedAmbiguousUsers} are updated appropriately. The write * lock of {@link #cacheLock} must already be acquired before invoking this * function. * * @param record - * The record to associate with the given username. + * The record to associate with the given user. * * @param username * The username that the given record should be associated with. This * may be null. + * + * @param domain + * The domain that the given record should be associated with. This + * may be null. */ - private void addRecordForLogin(KeeperRecord record, String username) { + private void addRecordForLogin( + KeeperRecord record, String username, String domain) { if (username == null) return; - KeeperRecord existing = cachedRecordsByUsername.putIfAbsent(username, record); + UserDomain userDomain = new UserDomain(username, domain); + KeeperRecord existing = cachedRecordsByUser.putIfAbsent( + userDomain, record); if (existing != null && record != existing) - cachedAmbiguousUsernames.add(username); + cachedAmbiguousUsers.add(userDomain); } @@ -489,32 +497,41 @@ public class KsmClient { } /** - * Returns the record associated with the given username. If no such record - * exists, or there are multiple such records, null is returned. + * Returns the record associated with the given username and domain. If no + * such record exists, or there are multiple such records, null is returned. * * @param username * The username of the record to return. * + * @param domain + * The domain of the record to return. + * * @return - * The record associated with the given username, or null if there is - * no such record or multiple such records. + * The record associated with the given username and domain, or null + * if there is no such record or multiple such records. * * @throws GuacamoleException * If an error occurs that prevents the record from being retrieved. */ - public KeeperRecord getRecordByLogin(String username) throws GuacamoleException { + public KeeperRecord getRecordByLogin( + String username, String domain) throws GuacamoleException { + validateCache(); cacheLock.readLock().lock(); + + UserDomain userDomain = new UserDomain(username, domain); + try { - if (cachedAmbiguousUsernames.contains(username)) { - logger.debug("The username \"{}\" is referenced by multiple " - + "Keeper records and cannot be used to locate " - + "individual secrets.", username); + if (cachedAmbiguousUsers.contains(userDomain)) { + logger.debug("The username \"{}\" with domain \"{}\" is " + + "referenced by multiple Keeper records and " + + "cannot be used to locate individual secrets.", + username, domain); return null; } - return cachedRecordsByUsername.get(username); + return cachedRecordsByUser.get(userDomain); } finally { diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 604ae2865..3b8a6dc0d 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -323,12 +323,6 @@ public class KsmSecretService implements VaultSecretService { addRecordTokens(tokens, "KEEPER_SERVER_", ksm.getRecordByHost(filter.filter(hostname))); - // Retrieve and define user-specific tokens, if any - String username = parameters.get("username"); - if (username != null && !username.isEmpty()) - addRecordTokens(tokens, "KEEPER_USER_", - ksm.getRecordByLogin(filter.filter(username))); - // Tokens specific to RDP if ("rdp".equals(config.getProtocol())) { @@ -338,24 +332,57 @@ public class KsmSecretService implements VaultSecretService { addRecordTokens(tokens, "KEEPER_GATEWAY_", ksm.getRecordByHost(filter.filter(gatewayHostname))); + // Retrieve and define domain tokens, if any + String domain = parameters.get("domain"); + String filteredDomain = null; + if (domain != null && !domain.isEmpty()) { + filteredDomain = filter.filter(domain); + addRecordTokens(tokens, "KEEPER_DOMAIN_", + ksm.getRecordByDomain(filteredDomain)); + } + + // Retrieve and define gateway domain tokens, if any + String gatewayDomain = parameters.get("gateway-domain"); + String filteredGatewayDomain = null; + if (gatewayDomain != null && !gatewayDomain.isEmpty()) { + filteredGatewayDomain = filter.filter(gatewayDomain); + addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_", + ksm.getRecordByDomain(filteredGatewayDomain)); + } + + // If domain matching is disabled for user records, + // explicitly set the domains to null when storing + // user records to enable username-only matching + if (!confService.getMatchUserRecordsByDomain()) { + filteredDomain = null; + filteredGatewayDomain = null; + } + + // Retrieve and define user-specific tokens, if any + String username = parameters.get("username"); + if (username != null && !username.isEmpty()) + addRecordTokens(tokens, "KEEPER_USER_", + ksm.getRecordByLogin(filter.filter(username), + filteredDomain)); + // Retrieve and define gateway user-specific tokens, if any String gatewayUsername = parameters.get("gateway-username"); if (gatewayUsername != null && !gatewayUsername.isEmpty()) addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", - ksm.getRecordByLogin(filter.filter(gatewayUsername))); + ksm.getRecordByLogin( + filter.filter(gatewayUsername), + filteredGatewayDomain)); - // Retrieve and define domain tokens, if any - String domain = parameters.get("domain"); - if (domain != null && !domain.isEmpty()) - addRecordTokens(tokens, "KEEPER_DOMAIN_", - ksm.getRecordByDomain(filter.filter(domain))); - - // Retrieve and define gateway domain tokens, if any - String gatewayDomain = parameters.get("gateway-domain"); - if (gatewayDomain != null && !gatewayDomain.isEmpty()) - addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_", - ksm.getRecordByDomain(filter.filter(gatewayDomain))); + } else { + // Retrieve and define user-specific tokens, if any + // NOTE that non-RDP connections do not have a domain + // field in the connection parameters, so the domain + // will always be null + String username = parameters.get("username"); + if (username != null && !username.isEmpty()) + addRecordTokens(tokens, "KEEPER_USER_", + ksm.getRecordByLogin(filter.filter(username), null)); } return tokens; diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserDomain.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserDomain.java new file mode 100644 index 000000000..bb2c2309d --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserDomain.java @@ -0,0 +1,129 @@ +/* + * 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.vault.ksm.secret; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A class intended for use as a key in KSM user record client cache. This + * class contains both a username and a password. When identifying a KSM + * record using token syntax like "KEEPER_USER_*", the user record will + * actually be identified by both the user and domain, if the appropriate + * settings are enabled. + */ +class UserDomain { + + /** + * The username associated with the user record. + * This field should never be null. + */ + private final String username; + + /** + * The domain associated with the user record. + * This field can be null. + */ + private final String domain; + + /** + * Create a new UserDomain instance with the provided username and + * domain. The domain may be null, but the username should never be. + * + * @param username + * The username to create the UserDomain instance with. This should + * never be null. + * + * @param domain + * The domain to create the UserDomain instance with. This can be null. + */ + UserDomain(@Nonnull String username, @Nullable String domain) { + this.username = username; + this.domain = domain; + } + + @Override + public int hashCode() { + + final int prime = 31; + + int result = 1; + result = prime * result + ((domain == null) ? 0 : domain.hashCode()); + result = prime * result + ((username == null) ? 0 : username.hashCode()); + + return result; + + } + + @Override + public boolean equals(Object obj) { + + // Check if the other object is this exact object + if (this == obj) + return true; + + // Check if the other object is null + if (obj == null) + return false; + + // Check if the other object is also a UserDomain + if (getClass() != obj.getClass()) + return false; + + // If it is a UserDomain, it must have the same username... + UserDomain other = (UserDomain) obj; + if (username == null) { + if (other.username != null) + return false; + } else if (!username.equals(other.username)) + return false; + + // .. and the same domain + if (domain == null) { + if (other.domain != null) + return false; + } else if (!domain.equals(other.domain)) + return false; + + return true; + } + + /** + * Get the username associated with this UserDomain. + * + * @return + * The username associated with this UserDomain. + */ + public String getUsername() { + return username; + } + + + /** + * Get the domain associated with this UserDomain. + * + * @return + * The domain associated with this UserDomain. + */ + public String getDomain() { + return domain; + } + +} \ No newline at end of file From d72acf6f29bc09238101a5546499d030aa1d12b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CVict0rC=E2=80=9D?= <“victor.cleaner@gmail.com”> Date: Fri, 12 Aug 2022 13:06:11 +0200 Subject: [PATCH 052/289] GUACAMOLE-1664: Completed Czech translation, minor corrections. Indentation of the text adjusted exactly according to the en.json file. --- .../main/frontend/src/translations/cs.json | 402 ++++++++++-------- 1 file changed, 220 insertions(+), 182 deletions(-) diff --git a/guacamole/src/main/frontend/src/translations/cs.json b/guacamole/src/main/frontend/src/translations/cs.json index 04c7a3a4b..c02e13c69 100644 --- a/guacamole/src/main/frontend/src/translations/cs.json +++ b/guacamole/src/main/frontend/src/translations/cs.json @@ -13,6 +13,7 @@ "ACTION_DELETE_SESSIONS" : "Ukončit sezení", "ACTION_DOWNLOAD" : "Stáhnout", "ACTION_LOGIN" : "Přihlásit", + "ACTION_LOGIN_AGAIN" : "Znovu přihlásit", "ACTION_LOGOUT" : "Odhlásit", "ACTION_MANAGE_CONNECTIONS" : "Připojení", "ACTION_MANAGE_PREFERENCES" : "Vlastnosti", @@ -22,11 +23,14 @@ "ACTION_MANAGE_USER_GROUPS" : "Skupiny", "ACTION_NAVIGATE_BACK" : "Zpět", "ACTION_NAVIGATE_HOME" : "Domů", + "ACTION_PAUSE" : "Pozastavit", + "ACTION_PLAY" : "Přehrát", "ACTION_SAVE" : "Uložit", "ACTION_SEARCH" : "Hledat", "ACTION_SHARE" : "Sdílet", "ACTION_UPDATE_PASSWORD" : "Změnit heslo", "ACTION_VIEW_HISTORY" : "Historie", + "ACTION_VIEW_RECORDING" : "Zobrazit", "DIALOG_HEADER_ERROR" : "Chyba", @@ -42,6 +46,7 @@ "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss", "INFO_ACTIVE_USER_COUNT" : "V současné době používá {USERS} {USERS, plural, one{uživatel} other{uživatelů}}.", + "INFO_LOGGED_OUT" : "Byli jste odhlášeni.", "TEXT_ANONYMOUS_USER" : "Neznámý uživatel", "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{sekundu} other{sekund}}} minute{{VALUE, plural, one{minutu} other{minut}}} hour{{VALUE, plural, one{hodinu} other{hodin}}} day{{VALUE, plural, one{den} other{dnů}}} other{}}", @@ -52,7 +57,9 @@ "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Vyčistit", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_DISCONNECT" : "Odpojit", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", @@ -380,33 +387,45 @@ }, + "PLAYER" : { + + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", + "ACTION_PLAY" : "@:APP.ACTION_PLAY", + + "INFO_LOADING_RECORDING" : "Vaše nahrávka se nyní načítá. Čekejte prosím...", + "INFO_SEEK_IN_PROGRESS" : "Hledání požadované pozice. Čekejte prosím..." + + }, + "PROTOCOL_KUBERNETES" : { - "FIELD_HEADER_BACKSPACE" : "Klávesa Zpět odešle:", - "FIELD_HEADER_CA_CERT" : "Certifikát certifikační autority:", - "FIELD_HEADER_CLIENT_CERT" : "Klientský certifikát:", - "FIELD_HEADER_CLIENT_KEY" : "Klientský klíč:", - "FIELD_HEADER_COLOR_SCHEME" : "Barevné schéma:", - "FIELD_HEADER_CONTAINER" : "Jméno kontejneru:", - "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu k záznamu:", - "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automaticky vytvořit cestu ke strojopisu:", - "FIELD_HEADER_FONT_NAME" : "Jméno fontu:", - "FIELD_HEADER_FONT_SIZE" : "Velikost fontu:", - "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_IGNORE_CERT" : "Ignorovat serverový certifikát:", - "FIELD_HEADER_NAMESPACE" : "Obor názvů:", - "FIELD_HEADER_POD" : "Jméno podu:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", + "FIELD_HEADER_BACKSPACE" : "Klávesa Zpět odešle:", + "FIELD_HEADER_CA_CERT" : "Certifikát certifikační autority:", + "FIELD_HEADER_CLIENT_CERT" : "Klientský certifikát:", + "FIELD_HEADER_CLIENT_KEY" : "Klientský klíč:", + "FIELD_HEADER_COLOR_SCHEME" : "Barevné schéma:", + "FIELD_HEADER_CONTAINER" : "Jméno kontejneru:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu k záznamu:", + "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automaticky vytvořit cestu ke strojopisu:", + "FIELD_HEADER_EXEC_COMMAND" : "Příkaz (exec):", + "FIELD_HEADER_FONT_NAME" : "Jméno fontu:", + "FIELD_HEADER_FONT_SIZE" : "Velikost fontu:", + "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", + "FIELD_HEADER_IGNORE_CERT" : "Ignorovat serverový certifikát:", + "FIELD_HEADER_NAMESPACE" : "Obor názvů:", + "FIELD_HEADER_POD" : "Jméno podu:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Vyloučit myš:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vyloučit grafiku/streamování:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Zahrnout události kláves:", - "FIELD_HEADER_RECORDING_NAME" : "Jméno záznamu:", - "FIELD_HEADER_RECORDING_PATH" : "Cesta k záznamu:", - "FIELD_HEADER_SCROLLBACK" : "Maximální délka historie:", - "FIELD_HEADER_TYPESCRIPT_NAME" : "Jméno strojopisu:", - "FIELD_HEADER_TYPESCRIPT_PATH" : "Cesta ke strojopisu:", - "FIELD_HEADER_USE_SSL" : "Použít SSL/TLS", + "FIELD_HEADER_RECORDING_NAME" : "Jméno záznamu:", + "FIELD_HEADER_RECORDING_PATH" : "Cesta k záznamu:", + "FIELD_HEADER_SCROLLBACK" : "Maximální délka historie:", + "FIELD_HEADER_TYPESCRIPT_NAME" : "Jméno strojopisu:", + "FIELD_HEADER_TYPESCRIPT_PATH" : "Cesta ke strojopisu:", + "FIELD_HEADER_USE_SSL" : "Použít SSL/TLS", "FIELD_OPTION_BACKSPACE_EMPTY" : "", "FIELD_OPTION_BACKSPACE_8" : "Zpět (Ctrl-H)", @@ -448,22 +467,22 @@ "PROTOCOL_RDP" : { - "FIELD_HEADER_CLIENT_NAME" : "Jméno klienta:", - "FIELD_HEADER_COLOR_DEPTH" : "Barevná hloubka:", - "FIELD_HEADER_CONSOLE" : "Konzola pro správu:", - "FIELD_HEADER_CONSOLE_AUDIO" : "Podpora zvuku v konzole:", - "FIELD_HEADER_CREATE_DRIVE_PATH" : "Automaticky vytvořit disk:", - "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu k záznamu:", - "FIELD_HEADER_DISABLE_AUDIO" : "Zakázat zvuk:", - "FIELD_HEADER_DISABLE_AUTH" : "Zakázat ověřování:", - "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování ze vzdálené plochy:", - "FIELD_HEADER_DISABLE_DOWNLOAD" : "Zakázat stahování souborů:", - "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", - "FIELD_HEADER_DISABLE_UPLOAD" : "Zakázat nahrávání souborů:", - "FIELD_HEADER_DOMAIN" : "Doména:", - "FIELD_HEADER_DPI" : "Rozlišení (DPI):", - "FIELD_HEADER_DRIVE_NAME" : "Název jednotky:", - "FIELD_HEADER_DRIVE_PATH" : "Cesta na disku:", + "FIELD_HEADER_CLIENT_NAME" : "Jméno klienta:", + "FIELD_HEADER_COLOR_DEPTH" : "Barevná hloubka:", + "FIELD_HEADER_CONSOLE" : "Konzola pro správu:", + "FIELD_HEADER_CONSOLE_AUDIO" : "Podpora zvuku v konzole:", + "FIELD_HEADER_CREATE_DRIVE_PATH" : "Automaticky vytvořit disk:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu k záznamu:", + "FIELD_HEADER_DISABLE_AUDIO" : "Zakázat zvuk:", + "FIELD_HEADER_DISABLE_AUTH" : "Zakázat ověřování:", + "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování ze vzdálené plochy:", + "FIELD_HEADER_DISABLE_DOWNLOAD" : "Zakázat stahování souborů:", + "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", + "FIELD_HEADER_DISABLE_UPLOAD" : "Zakázat nahrávání souborů:", + "FIELD_HEADER_DOMAIN" : "Doména:", + "FIELD_HEADER_DPI" : "Rozlišení (DPI):", + "FIELD_HEADER_DRIVE_NAME" : "Název jednotky:", + "FIELD_HEADER_DRIVE_PATH" : "Cesta na disku:", "FIELD_HEADER_ENABLE_AUDIO_INPUT" : "Povolit zvukový vstup (mikrofon):", "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Povolit kompozici pracovní plochy (Aero):", "FIELD_HEADER_ENABLE_DRIVE" : "Povolit jednotku:", @@ -473,42 +492,47 @@ "FIELD_HEADER_DISABLE_BITMAP_CACHING" : "Zakázat ukládání do mezipaměti bitmap:", "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING" : "Zakázat ukládání do mezipaměti mimo obrazovku:", "FIELD_HEADER_DISABLE_GLYPH_CACHING" : "Zakázat ukládání do mezipaměti glyfů:", + "FIELD_HEADER_DISABLE_GFX" : "Zakázat rozšíření Graphics Pipeline:", "FIELD_HEADER_ENABLE_PRINTING" : "Povolit tisk:", - "FIELD_HEADER_ENABLE_SFTP" : "Povolit SFTP:", + "FIELD_HEADER_ENABLE_SFTP" : "Povolit SFTP:", "FIELD_HEADER_ENABLE_THEMING" : "Povolit motivy:", + "FIELD_HEADER_ENABLE_TOUCH" : "Povolit vícedotykové ovládání:", "FIELD_HEADER_ENABLE_WALLPAPER" : "Povolit tapetu:", - "FIELD_HEADER_GATEWAY_DOMAIN" : "Doména:", - "FIELD_HEADER_GATEWAY_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_GATEWAY_PASSWORD" : "Heslo:", - "FIELD_HEADER_GATEWAY_PORT" : "Port:", - "FIELD_HEADER_GATEWAY_USERNAME" : "Uživatelské jméno:", - "FIELD_HEADER_HEIGHT" : "Výška:", - "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_IGNORE_CERT" : "Ignorovat serverový certifikát:", - "FIELD_HEADER_INITIAL_PROGRAM" : "Úvodní program:", - "FIELD_HEADER_LOAD_BALANCE_INFO" : "Vyvážení zátěže info/cookie:", - "FIELD_HEADER_PASSWORD" : "Heslo:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_PRINTER_NAME" : "Název přesměrované tiskárny:", - "FIELD_HEADER_PRECONNECTION_BLOB" : "Preconnection BLOB (VM ID):", - "FIELD_HEADER_PRECONNECTION_ID" : "Zdrojové ID RDP:", - "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", - "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Vyloučit myš:", - "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vyloučit grafiku/strímování:", - "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Zahrnout klíčové události:", - "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", - "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", - "FIELD_HEADER_RESIZE_METHOD" : "Metoda změny velikosti:", - "FIELD_HEADER_REMOTE_APP_ARGS" : "Parametry:", - "FIELD_HEADER_REMOTE_APP_DIR" : "Pracovní adresář:", - "FIELD_HEADER_REMOTE_APP" : "Program:", - "FIELD_HEADER_SECURITY" : "Bezpečnostní mód:", - "FIELD_HEADER_SERVER_LAYOUT" : "Rozložení klávesnice:", + "FIELD_HEADER_FORCE_LOSSLESS" : "Vynutit bezeztrátovou kompresi:", + "FIELD_HEADER_GATEWAY_DOMAIN" : "Doména:", + "FIELD_HEADER_GATEWAY_HOSTNAME" : "Jméno hostitele:", + "FIELD_HEADER_GATEWAY_PASSWORD" : "Heslo:", + "FIELD_HEADER_GATEWAY_PORT" : "Port:", + "FIELD_HEADER_GATEWAY_USERNAME" : "Uživatelské jméno:", + "FIELD_HEADER_HEIGHT" : "Výška:", + "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", + "FIELD_HEADER_IGNORE_CERT" : "Ignorovat serverový certifikát:", + "FIELD_HEADER_INITIAL_PROGRAM" : "Úvodní program:", + "FIELD_HEADER_LOAD_BALANCE_INFO" : "Vyvážení zátěže info/cookie:", + "FIELD_HEADER_NORMALIZE_CLIPBOARD" : "Konce řádků:", + "FIELD_HEADER_PASSWORD" : "Heslo:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_PRINTER_NAME" : "Název přesměrované tiskárny:", + "FIELD_HEADER_PRECONNECTION_BLOB" : "Preconnection BLOB (VM ID):", + "FIELD_HEADER_PRECONNECTION_ID" : "Zdrojové ID RDP:", + "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", + "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Vyloučit myš:", + "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vyloučit grafiku/strímování:", + "FIELD_HEADER_RECORDING_EXCLUDE_TOUCH" : "Vyloučit dotykové události:", + "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Zahrnout klíčové události:", + "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", + "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", + "FIELD_HEADER_RESIZE_METHOD" : "Metoda změny velikosti:", + "FIELD_HEADER_REMOTE_APP_ARGS" : "Parametry:", + "FIELD_HEADER_REMOTE_APP_DIR" : "Pracovní adresář:", + "FIELD_HEADER_REMOTE_APP" : "Program:", + "FIELD_HEADER_SECURITY" : "Bezpečnostní mód:", + "FIELD_HEADER_SERVER_LAYOUT" : "Rozložení klávesnice:", "FIELD_HEADER_SFTP_DIRECTORY" : "Výchozí složka pro uložení záznamu:", "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Zakázat stahování souborů:", "FIELD_HEADER_SFTP_HOST_KEY" : "Veřejný klíč hosta (Base64):", "FIELD_HEADER_SFTP_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive interval:", + "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Interval udržování SFTP:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Přístupová fráze:", "FIELD_HEADER_SFTP_PASSWORD" : "Heslo:", "FIELD_HEADER_SFTP_PORT" : "Port:", @@ -516,14 +540,20 @@ "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Souborový prohlížeč kořenové složky:", "FIELD_HEADER_SFTP_DISABLE_UPLOAD" : "Zakázat nahrávání souborů:", "FIELD_HEADER_SFTP_USERNAME" : "Uživatelské jméno:", - "FIELD_HEADER_STATIC_CHANNELS" : "Názvy statických kanálů:", - "FIELD_HEADER_TIMEZONE" : "Časová zóna:", - "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", - "FIELD_HEADER_WIDTH" : "Šířka:", - "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", - "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", - "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", - "FIELD_HEADER_WOL_WAIT_TIME" : "Čekací doba potřebná pro spuštení hosta:", + "FIELD_HEADER_STATIC_CHANNELS" : "Názvy statických kanálů:", + "FIELD_HEADER_TIMEZONE" : "Časová zóna:", + "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", + "FIELD_HEADER_WIDTH" : "Šířka:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "UDP port pro paket WoL: ", + "FIELD_HEADER_WOL_WAIT_TIME" : "Doba čekání na spuštění hostitele:", + + "FIELD_OPTION_NORMALIZE_CLIPBOARD_EMPTY" : "", + "FIELD_OPTION_NORMALIZE_CLIPBOARD_PRESERVE" : "Zachovat tak, jak je", + "FIELD_OPTION_NORMALIZE_CLIPBOARD_UNIX" : "Linux/Mac/Unix (LF)", + "FIELD_OPTION_NORMALIZE_CLIPBOARD_WINDOWS" : "Windows (CRLF)", "FIELD_OPTION_COLOR_DEPTH_16" : "Nízké barvy (16-bitů)", "FIELD_OPTION_COLOR_DEPTH_24" : "Opravdové barvy (24-bitů)", @@ -542,25 +572,27 @@ "FIELD_OPTION_SECURITY_TLS" : "Šifrování TLS", "FIELD_OPTION_SECURITY_VMCONNECT" : "Hyper-V / VMConnect", - "FIELD_OPTION_SERVER_LAYOUT_DE_CH_QWERTZ" : "Švícarská Němčina (Qwertz)", - "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "Němčina (Qwertz)", - "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", - "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK Angličtina (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US Angličtina (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_ES_ES_QWERTY" : "Španělština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_DE_CH_QWERTZ" : "Švícarská Němčina (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "Němčina (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", + "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK Angličtina (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US Angličtina (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_ES_ES_QWERTY" : "Španělština (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Standardní Angličtina (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", - "FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Belgická Francouzština (Azerty)", - "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Švícarská Francouzština (Qwertz)", - "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Francouzština (Azerty)", - "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Maďarština (Qwertz)", - "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italština (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japonština (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_PL_PL_QWERTY" : "Polské (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portugalská Brazilština (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Švédština (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Dánština (Qwerty)", - "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turečtina (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", + "FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Belgická Francouzština (Azerty)", + "FIELD_OPTION_SERVER_LAYOUT_FR_CA_QWERTY" : "Kanadská francouzština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Švícarská Francouzština (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Francouzština (Azerty)", + "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Maďarština (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japonština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_NO_NO_QWERTY" : "Norština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_PL_PL_QWERTY" : "Polské (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portugalská Brazilština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Švédština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Dánština (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turečtina (Qwerty)", "NAME" : "RDP", @@ -583,43 +615,44 @@ "PROTOCOL_SSH" : { - "FIELD_HEADER_BACKSPACE" : "Klávesa Zpět odešle:", - "FIELD_HEADER_COLOR_SCHEME" : "Barva:", - "FIELD_HEADER_COMMAND" : "Provést příkaz:", - "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu pro uložení záznamu:", - "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automaticky vytvořit cestu ke strojopisu:", - "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování ze vzdáleného terminálu:", - "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", - "FIELD_HEADER_FONT_NAME" : "Typ fontu:", - "FIELD_HEADER_FONT_SIZE" : "Velikost písma:", - "FIELD_HEADER_ENABLE_SFTP" : "Povolit SFTP:", - "FIELD_HEADER_HOST_KEY" : "Veřejný klíč hosta (Base64):", - "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_LOCALE" : "Jazyk/Lokalizace ($LANG):", - "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", - "FIELD_HEADER_PASSWORD" : "Heslo:", - "FIELD_HEADER_PASSPHRASE" : "Přístupová fráze:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_PRIVATE_KEY" : "Privátní klíč:", - "FIELD_HEADER_SCROLLBACK" : "Maximální délka historie:", - "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", + "FIELD_HEADER_BACKSPACE" : "Klávesa Zpět odešle:", + "FIELD_HEADER_COLOR_SCHEME" : "Barva:", + "FIELD_HEADER_COMMAND" : "Provést příkaz:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu pro uložení záznamu:", + "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automaticky vytvořit cestu ke strojopisu:", + "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování ze vzdáleného terminálu:", + "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", + "FIELD_HEADER_FONT_NAME" : "Typ fontu:", + "FIELD_HEADER_FONT_SIZE" : "Velikost písma:", + "FIELD_HEADER_ENABLE_SFTP" : "Povolit SFTP:", + "FIELD_HEADER_HOST_KEY" : "Veřejný klíč hosta (Base64):", + "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", + "FIELD_HEADER_LOCALE" : "Jazyk/Lokalizace ($LANG):", + "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", + "FIELD_HEADER_PASSWORD" : "Heslo:", + "FIELD_HEADER_PASSPHRASE" : "Přístupová fráze:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_PRIVATE_KEY" : "Privátní klíč:", + "FIELD_HEADER_SCROLLBACK" : "Maximální délka historie:", + "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Vynechat myš:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vynechat grafiku/streamování:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Zahrnout události kláves:", - "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", - "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", - "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "Serverový keepalive interval:", - "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Zakázat stahování souborů:", - "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Souborový prohlížeč kořenové složky:", - "FIELD_HEADER_SFTP_DISABLE_UPLOAD" : "Zakázat nahrávání souborů:", - "FIELD_HEADER_TERMINAL_TYPE" : "Typ terminálu:", - "FIELD_HEADER_TIMEZONE" : "Časová zóna ($TZ):", - "FIELD_HEADER_TYPESCRIPT_NAME" : "Jméno strojopisu:", - "FIELD_HEADER_TYPESCRIPT_PATH" : "Cesta ke strojopisu:", - "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", - "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", - "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", - "FIELD_HEADER_WOL_WAIT_TIME" : "Čekací doba potřebná pro spuštení hosta:", + "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", + "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", + "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "Serverový keepalive interval:", + "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Zakázat stahování souborů:", + "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Souborový prohlížeč kořenové složky:", + "FIELD_HEADER_SFTP_DISABLE_UPLOAD" : "Zakázat nahrávání souborů:", + "FIELD_HEADER_TERMINAL_TYPE" : "Typ terminálu:", + "FIELD_HEADER_TIMEZONE" : "Časová zóna ($TZ):", + "FIELD_HEADER_TYPESCRIPT_NAME" : "Jméno strojopisu:", + "FIELD_HEADER_TYPESCRIPT_PATH" : "Cesta ke strojopisu:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "UDP port pro paket WoL:", + "FIELD_HEADER_WOL_WAIT_TIME" : "Doba čekání na spuštění hostitele:", "FIELD_OPTION_BACKSPACE_EMPTY" : "", "FIELD_OPTION_BACKSPACE_8" : "Zpět (Ctrl-H)", @@ -672,36 +705,37 @@ "PROTOCOL_TELNET" : { - "FIELD_HEADER_BACKSPACE" : "Klávesa Zpět odešle:", - "FIELD_HEADER_COLOR_SCHEME" : "Barevné schéma:", - "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu pro uložení záznamu:", - "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automaticky vytvořit cestu ke strojopisu:", - "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování z terminálu:", - "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", - "FIELD_HEADER_FONT_NAME" : "Jméno fontu:", - "FIELD_HEADER_FONT_SIZE" : "Velikost fontu:", - "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_LOGIN_FAILURE_REGEX" : "Selhání přihlášení regulární výraz:", - "FIELD_HEADER_LOGIN_SUCCESS_REGEX" : "Úspěch přihlášení regulární výraz:", - "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", - "FIELD_HEADER_USERNAME_REGEX" : "Uživatelské jméno regulární výraz:", - "FIELD_HEADER_PASSWORD" : "Heslo:", - "FIELD_HEADER_PASSWORD_REGEX" : "Heslo regulární výraz:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", + "FIELD_HEADER_BACKSPACE" : "Klávesa Zpět odešle:", + "FIELD_HEADER_COLOR_SCHEME" : "Barevné schéma:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu pro uložení záznamu:", + "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automaticky vytvořit cestu ke strojopisu:", + "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování z terminálu:", + "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", + "FIELD_HEADER_FONT_NAME" : "Jméno fontu:", + "FIELD_HEADER_FONT_SIZE" : "Velikost fontu:", + "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", + "FIELD_HEADER_LOGIN_FAILURE_REGEX" : "Přihlášení se nezdařilo regulární výraz:", + "FIELD_HEADER_LOGIN_SUCCESS_REGEX" : "Přihlášení bylo úspěšné regulární výraz:", + "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", + "FIELD_HEADER_USERNAME_REGEX" : "Uživatelské jméno regulární výraz:", + "FIELD_HEADER_PASSWORD" : "Heslo:", + "FIELD_HEADER_PASSWORD_REGEX" : "Heslo regulární výraz:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_READ_ONLY" : "Pouze ke čtení:", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Vyloučit myš:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vyloučit grafiku/streamování:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Zahrnout klíčové události:", - "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", - "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", - "FIELD_HEADER_SCROLLBACK" : "Maximální délka historie:", - "FIELD_HEADER_TERMINAL_TYPE" : "Typ terminálu:", - "FIELD_HEADER_TYPESCRIPT_NAME" : "Jméno strojopisu:", - "FIELD_HEADER_TYPESCRIPT_PATH" : "Cesta ke strojopisu:", - "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", - "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", - "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", - "FIELD_HEADER_WOL_WAIT_TIME" : "Čekací doba potřebná pro spuštení hosta:", + "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", + "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", + "FIELD_HEADER_SCROLLBACK" : "Maximální délka historie:", + "FIELD_HEADER_TERMINAL_TYPE" : "Typ terminálu:", + "FIELD_HEADER_TYPESCRIPT_NAME" : "Jméno strojopisu:", + "FIELD_HEADER_TYPESCRIPT_PATH" : "Cesta ke strojopisu:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "UDP port pro paket WoL:", + "FIELD_HEADER_WOL_WAIT_TIME" : "Doba čekání na spuštění hostitele:", "FIELD_OPTION_BACKSPACE_EMPTY" : "", "FIELD_OPTION_BACKSPACE_8" : "Zpět (Ctrl-H)", @@ -752,32 +786,33 @@ "PROTOCOL_VNC" : { - "FIELD_HEADER_AUDIO_SERVERNAME" : "Název zvukového serveru:", - "FIELD_HEADER_CLIPBOARD_ENCODING" : "Kódovávání:", - "FIELD_HEADER_COLOR_DEPTH" : "Hloubka barev:", - "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu pro uložení záznamu:", - "FIELD_HEADER_CURSOR" : "Kurzor:", - "FIELD_HEADER_DEST_HOST" : "Cílový host:", - "FIELD_HEADER_DEST_PORT" : "Vzdálený port:", - "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování ze vzdálené plochy:", - "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", - "FIELD_HEADER_ENABLE_AUDIO" : "Zapnout audio:", - "FIELD_HEADER_ENABLE_SFTP" : "Povolit SFTP:", - "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", - "FIELD_HEADER_PASSWORD" : "Heslo:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_READ_ONLY" : "Pouze čtení:", - "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Vynechat myš:", - "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vynechat grafiku/stremování:", - "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Zahrnout události kláves:", - "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", - "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", + "FIELD_HEADER_AUDIO_SERVERNAME" : "Název zvukového serveru:", + "FIELD_HEADER_CLIPBOARD_ENCODING" : "Kódovávání:", + "FIELD_HEADER_COLOR_DEPTH" : "Hloubka barev:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automaticky vytvořit cestu pro uložení záznamu:", + "FIELD_HEADER_CURSOR" : "Kurzor:", + "FIELD_HEADER_DEST_HOST" : "Cílový host:", + "FIELD_HEADER_DEST_PORT" : "Vzdálený port:", + "FIELD_HEADER_DISABLE_COPY" : "Zakázat kopírování ze vzdálené plochy:", + "FIELD_HEADER_DISABLE_PASTE" : "Zakázat vkládání z klienta:", + "FIELD_HEADER_ENABLE_AUDIO" : "Zapnout audio:", + "FIELD_HEADER_ENABLE_SFTP" : "Povolit SFTP:", + "FIELD_HEADER_FORCE_LOSSLESS" : "Vynutit bezeztrátovou kompresi:", + "FIELD_HEADER_HOSTNAME" : "Jméno hostitele:", + "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", + "FIELD_HEADER_PASSWORD" : "Heslo:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_READ_ONLY" : "Pouze čtení:", + "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Vynechat myš:", + "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vynechat grafiku/stremování:", + "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Zahrnout události kláves:", + "FIELD_HEADER_RECORDING_NAME" : "Název záznamu:", + "FIELD_HEADER_RECORDING_PATH" : "Cesta pro záznam:", "FIELD_HEADER_SFTP_DIRECTORY" : "Výchozí složka pro uložení záznamu:", "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Zakázat stahování souborů:", "FIELD_HEADER_SFTP_HOST_KEY" : "Veřejný klíč hosta (Base64):", "FIELD_HEADER_SFTP_HOSTNAME" : "Jméno hostitele:", - "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive interval:", + "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Interval udržování SFTP:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Přístupová fráze:", "FIELD_HEADER_SFTP_PASSWORD" : "Heslo:", "FIELD_HEADER_SFTP_PORT" : "Port:", @@ -785,11 +820,12 @@ "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Souborový prohlížeč kořenové složky:", "FIELD_HEADER_SFTP_DISABLE_UPLOAD" : "Zakázat nahrávání souborů:", "FIELD_HEADER_SFTP_USERNAME" : "Uživatelské jméno:", - "FIELD_HEADER_SWAP_RED_BLUE" : "Přehodit červené/modré komponenty:", - "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", - "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", - "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", - "FIELD_HEADER_WOL_WAIT_TIME" : "Čekací doba potřebná pro spuštení hosta:", + "FIELD_HEADER_SWAP_RED_BLUE" : "Přehodit červené/modré komponenty:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Vysílací adresa pro paket WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "MAC adresa vzdáleného hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Odeslat paket WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "UDP port pro paket WoL:", + "FIELD_HEADER_WOL_WAIT_TIME" : "Doba čekání na spuštění hostitele:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 barev", "FIELD_OPTION_COLOR_DEPTH_16" : "Nízké barvy (16-bitů)", @@ -831,6 +867,7 @@ "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", @@ -845,8 +882,9 @@ "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Název připojení", "TABLE_HEADER_SESSION_DURATION" : "Doba trvání", + "TABLE_HEADER_SESSION_LOGS" : "Protokoly", "TABLE_HEADER_SESSION_REMOTEHOST" : "Vzdálený host", - "TABLE_HEADER_SESSION_STARTDATE" : "Počáteční čas", + "TABLE_HEADER_SESSION_STARTDATE" : "Doba spuštění", "TABLE_HEADER_SESSION_USERNAME" : "Uživatelské jméno", "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" From 0e5a3cb74f275ba1477bd13f6b1af27d30426aa0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 18 Aug 2022 12:26:55 -0700 Subject: [PATCH 053/289] GUACAMOLE-990: The UserContext passed to redecorate() should NOT be the internal DecoratedUserContext wrapper. --- .../org/apache/guacamole/rest/auth/DecoratedUserContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java index d773068f3..9aa26fafc 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java @@ -145,7 +145,7 @@ public class DecoratedUserContext extends DelegatingUserContext { if (authProvider != userContext.getAuthenticationProvider()) { // Apply next layer of wrapping around UserContext - UserContext redecorated = authProvider.redecorate(decorated, + UserContext redecorated = authProvider.redecorate(decorated.getDelegateUserContext(), userContext, authenticatedUser, credentials); // Do not allow misbehaving extensions to wipe out the From e6a61b72237f38cb52f2def3e0911b0d9c3027f2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 18 Aug 2022 12:20:32 -0700 Subject: [PATCH 054/289] GUACAMOLE-990: Fire auth success/failure events only after authentication has absolutely succeeded or failed, including the details of any failure. Previously, these events were fired only after the user's identity had been determined (or failed to be determined). If we don't wait until after the user contexts have also been successfully obtained (or failed to be obtained), then things like MFA will not be taken into account for auth events. --- .../net/event/AuthenticationFailureEvent.java | 104 +++++++- .../event/AuthenticationProviderEvent.java | 40 +++ .../net/event/AuthenticationSuccessEvent.java | 11 +- .../guacamole/net/event/FailureEvent.java | 39 +++ .../rest/auth/AuthenticationService.java | 240 +++++++++--------- .../rest/auth/DecoratedUserContext.java | 48 ++-- .../rest/auth/DecorationService.java | 9 +- ...acamoleAuthenticationProcessException.java | 164 ++++++++++++ 8 files changed, 509 insertions(+), 146 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationProviderEvent.java create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/FailureEvent.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java index 9808e7047..870590076 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java @@ -19,28 +19,91 @@ package org.apache.guacamole.net.event; +import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.event.listener.Listener; /** * An event which is triggered whenever a user's credentials fail to be * authenticated. The credentials that failed to be authenticated are included * within this event, and can be retrieved using getCredentials(). */ -public class AuthenticationFailureEvent implements CredentialEvent { +public class AuthenticationFailureEvent implements AuthenticationProviderEvent, + CredentialEvent, FailureEvent { /** * The credentials which failed authentication. */ - private Credentials credentials; + private final Credentials credentials; /** - * Creates a new AuthenticationFailureEvent which represents the failure - * to authenticate the given credentials. + * The AuthenticationProvider that encountered the failure. This may be + * null if the AuthenticationProvider is not known, such as if the failure + * is caused by every AuthenticationProvider passively refusing to + * authenticate the user but without explicitly rejecting the user + * (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}), + * or if the failure is external to any installed AuthenticationProvider + * (such as within a {@link Listener}. + */ + private final AuthenticationProvider authProvider; + + /** + * The Throwable that was thrown resulting in the failure, if any. This + * may be null if authentication failed without a known error, such as if + * the failure is caused by every AuthenticationProvider passively refusing + * to authenticate the user but without explicitly rejecting the user + * (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}). + */ + private final Throwable failure; + + /** + * Creates a new AuthenticationFailureEvent which represents a failure + * to authenticate the given credentials where there is no specific + * AuthenticationProvider nor Throwable associated with the failure. * - * @param credentials The credentials which failed authentication. + * @param credentials + * The credentials which failed authentication. */ public AuthenticationFailureEvent(Credentials credentials) { + this(credentials, null); + } + + /** + * Creates a new AuthenticationFailureEvent which represents a failure + * to authenticate the given credentials where there is no specific + * AuthenticationProvider causing the failure. + * + * @param credentials + * The credentials which failed authentication. + * + * @param failure + * The Throwable that was thrown resulting in the failure, or null if + * there is no such Throwable. + */ + public AuthenticationFailureEvent(Credentials credentials, Throwable failure) { + this(credentials, null, failure); + } + + /** + * Creates a new AuthenticationFailureEvent which represents a failure + * to authenticate the given credentials. + * + * @param credentials + * The credentials which failed authentication. + * + * @param authProvider + * The AuthenticationProvider that caused the failure, or null if there + * is no such AuthenticationProvider. + * + * @param failure + * The Throwable that was thrown resulting in the failure, or null if + * there is no such Throwable. + */ + public AuthenticationFailureEvent(Credentials credentials, + AuthenticationProvider authProvider, Throwable failure) { this.credentials = credentials; + this.authProvider = authProvider; + this.failure = failure; } @Override @@ -48,4 +111,35 @@ public class AuthenticationFailureEvent implements CredentialEvent { return credentials; } + /** + * {@inheritDoc} + * + *

NOTE: In the case of an authentication failure, cases where this may + * be null include if authentication failed without a definite single + * AuthenticationProvider causing that failure, such as if the failure is + * caused by every AuthenticationProvider passively refusing to + * authenticate the user but without explicitly rejecting the user + * (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}), + * or if the failure is external to any installed AuthenticationProvider + * (such as within a {@link Listener}. + */ + @Override + public AuthenticationProvider getAuthenticationProvider() { + return authProvider; + } + + /** + * {@inheritDoc} + * + *

NOTE: In the case of an authentication failure, cases where this may + * be null include if authentication failed without a known error, such as + * if the failure is caused by every AuthenticationProvider passively + * refusing to authenticate the user but without explicitly rejecting the + * user (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}). + */ + @Override + public Throwable getFailure() { + return failure; + } + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationProviderEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationProviderEvent.java new file mode 100644 index 000000000..7faa87697 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationProviderEvent.java @@ -0,0 +1,40 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.AuthenticationProvider; + +/** + * An event which may be dispatched due to a specific AuthenticationProvider. + */ +public interface AuthenticationProviderEvent { + + /** + * Returns the AuthenticationProvider that resulted in the event, if any. + * If the event occurred without any definite causing + * AuthenticationProvider, this may be null. + * + * @return + * The AuthenticationProvider that resulted in the event, or null if no + * such AuthenticationProvider is known. + */ + AuthenticationProvider getAuthenticationProvider(); + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java index 8b63bcf0e..a9b21dce8 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java @@ -20,6 +20,7 @@ package org.apache.guacamole.net.event; import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; /** @@ -32,7 +33,8 @@ import org.apache.guacamole.net.auth.Credentials; * is effectively vetoed and will be subsequently processed as though the * authentication failed. */ -public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent { +public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent, + AuthenticationProviderEvent { /** * The AuthenticatedUser identifying the user that successfully @@ -60,7 +62,12 @@ public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent { @Override public Credentials getCredentials() { - return authenticatedUser.getCredentials(); + return getAuthenticatedUser().getCredentials(); + } + + @Override + public AuthenticationProvider getAuthenticationProvider() { + return getAuthenticatedUser().getAuthenticationProvider(); } } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/FailureEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/FailureEvent.java new file mode 100644 index 000000000..dfc337594 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/FailureEvent.java @@ -0,0 +1,39 @@ +/* + * 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.event; + +/** + * An event which represents failure of an operation, where that failure may + * be associated with a particular Throwable. + */ +public interface FailureEvent { + + /** + * Returns the Throwable that represents the failure that occurred, if any. + * If the failure was recognized but without a definite known error, this + * may be null. + * + * @return + * The Throwable that represents the failure that occurred, or null if + * no such Throwable is known. + */ + Throwable getFailure(); + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java index ce8a9fb0c..8ed76c6a6 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java @@ -34,7 +34,6 @@ import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.UserContext; -import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; @@ -169,14 +168,15 @@ public class AuthenticationService { * The AuthenticatedUser given by the highest-priority * AuthenticationProvider for which the given credentials are valid. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If the given credentials are not valid for any * AuthenticationProvider, or if an error occurs while authenticating * the user. */ private AuthenticatedUser authenticateUser(Credentials credentials) - throws GuacamoleException { + throws GuacamoleAuthenticationProcessException { + AuthenticationProvider failedAuthProvider = null; GuacamoleCredentialsException authFailure = null; // Attempt authentication against each AuthenticationProvider @@ -191,27 +191,29 @@ public class AuthenticationService { // Insufficient credentials should take precedence catch (GuacamoleInsufficientCredentialsException e) { - if (authFailure == null || authFailure instanceof GuacamoleInvalidCredentialsException) + if (authFailure == null || authFailure instanceof GuacamoleInvalidCredentialsException) { + failedAuthProvider = authProvider; authFailure = e; + } } - + // Catch other credentials exceptions and assign the first one catch (GuacamoleCredentialsException e) { - if (authFailure == null) + if (authFailure == null) { + failedAuthProvider = authProvider; authFailure = e; + } + } + + catch (GuacamoleException | RuntimeException | Error e) { + throw new GuacamoleAuthenticationProcessException("User " + + "authentication was aborted.", authProvider, e); } } - // If a specific failure occured, rethrow that - if (authFailure != null) - throw authFailure; - - // Otherwise, request standard username/password - throw new GuacamoleInvalidCredentialsException( - "Permission Denied.", - CredentialsInfo.USERNAME_PASSWORD - ); + throw new GuacamoleAuthenticationProcessException("User authentication " + + "failed.", failedAuthProvider, authFailure); } @@ -230,51 +232,29 @@ public class AuthenticationService { * A AuthenticatedUser which may have been updated due to re- * authentication. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If an error prevents the user from being re-authenticated. */ private AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { // Get original AuthenticationProvider AuthenticationProvider authProvider = authenticatedUser.getAuthenticationProvider(); - // Re-authenticate the AuthenticatedUser against the original AuthenticationProvider only - authenticatedUser = authProvider.updateAuthenticatedUser(authenticatedUser, credentials); - if (authenticatedUser == null) - throw new GuacamoleSecurityException("User re-authentication failed."); + try { - return authenticatedUser; + // Re-authenticate the AuthenticatedUser against the original AuthenticationProvider only + authenticatedUser = authProvider.updateAuthenticatedUser(authenticatedUser, credentials); + if (authenticatedUser == null) + throw new GuacamoleSecurityException("User re-authentication failed."); - } + return authenticatedUser; - /** - * Notify all bound listeners that a successful authentication - * has occurred. - * - * @param authenticatedUser - * The user that was successfully authenticated. - * - * @throws GuacamoleException - * If thrown by a listener. - */ - private void fireAuthenticationSuccessEvent(AuthenticatedUser authenticatedUser) - throws GuacamoleException { - listenerService.handleEvent(new AuthenticationSuccessEvent(authenticatedUser)); - } + } + catch (GuacamoleException | RuntimeException | Error e) { + throw new GuacamoleAuthenticationProcessException("User re-authentication failed.", authProvider, e); + } - /** - * Notify all bound listeners that an authentication attempt has failed. - * - * @param credentials - * The credentials that failed to authenticate. - * - * @throws GuacamoleException - * If thrown by a listener. - */ - private void fireAuthenticationFailedEvent(Credentials credentials) - throws GuacamoleException { - listenerService.handleEvent(new AuthenticationFailureEvent(credentials)); } /** @@ -292,61 +272,23 @@ public class AuthenticationService { * The AuthenticatedUser associated with the given session and * credentials. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If an error occurs while authenticating or re-authenticating the * user. */ private AuthenticatedUser getAuthenticatedUser(GuacamoleSession existingSession, - Credentials credentials) throws GuacamoleException { - - try { - - // Re-authenticate user if session exists - if (existingSession != null) { - AuthenticatedUser updatedUser = updateAuthenticatedUser( - existingSession.getAuthenticatedUser(), credentials); - fireAuthenticationSuccessEvent(updatedUser); - return updatedUser; - } - - // Otherwise, attempt authentication as a new user - AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials); - fireAuthenticationSuccessEvent(authenticatedUser); - - if (logger.isInfoEnabled()) - logger.info("User \"{}\" successfully authenticated from {}.", - authenticatedUser.getIdentifier(), - getLoggableAddress(credentials.getRequest())); - - return authenticatedUser; + Credentials credentials) throws GuacamoleAuthenticationProcessException { + // Re-authenticate user if session exists + if (existingSession != null) { + AuthenticatedUser updatedUser = updateAuthenticatedUser( + existingSession.getAuthenticatedUser(), credentials); + return updatedUser; } - // Log and rethrow any authentication errors - catch (GuacamoleException e) { - - fireAuthenticationFailedEvent(credentials); - - // Get request and username for sake of logging - HttpServletRequest request = credentials.getRequest(); - String username = credentials.getUsername(); - - // Log authentication failures with associated usernames - if (username != null) { - if (logger.isWarnEnabled()) - logger.warn("Authentication attempt from {} for user \"{}\" failed.", - getLoggableAddress(request), username); - } - - // Log anonymous authentication failures - else if (logger.isDebugEnabled()) - logger.debug("Anonymous authentication attempt from {} failed.", - getLoggableAddress(request)); - - // Rethrow exception - throw e; - - } + // Otherwise, attempt authentication as a new user + AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials); + return authenticatedUser; } @@ -371,15 +313,14 @@ public class AuthenticationService { * A List of all UserContexts associated with the given * AuthenticatedUser. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If an error occurs while creating or updating any UserContext. */ private List getUserContexts(GuacamoleSession existingSession, AuthenticatedUser authenticatedUser, Credentials credentials) - throws GuacamoleException { + throws GuacamoleAuthenticationProcessException { - List userContexts = - new ArrayList(authProviders.size()); + List userContexts = new ArrayList<>(authProviders.size()); // If UserContexts already exist, update them and add to the list if (existingSession != null) { @@ -392,7 +333,15 @@ public class AuthenticationService { // Update existing UserContext AuthenticationProvider authProvider = oldUserContext.getAuthenticationProvider(); - UserContext updatedUserContext = authProvider.updateUserContext(oldUserContext, authenticatedUser, credentials); + UserContext updatedUserContext; + try { + updatedUserContext = authProvider.updateUserContext(oldUserContext, authenticatedUser, credentials); + } + catch (GuacamoleException | RuntimeException | Error e) { + throw new GuacamoleAuthenticationProcessException("User " + + "authentication aborted during UserContext update.", + authProvider, e); + } // Add to available data, if successful if (updatedUserContext != null) @@ -415,7 +364,15 @@ public class AuthenticationService { for (AuthenticationProvider authProvider : authProviders) { // Generate new UserContext - UserContext userContext = authProvider.getUserContext(authenticatedUser); + UserContext userContext; + try { + userContext = authProvider.getUserContext(authenticatedUser); + } + catch (GuacamoleException | RuntimeException | Error e) { + throw new GuacamoleAuthenticationProcessException("User " + + "authentication aborted during initial " + + "UserContext creation.", authProvider, e); + } // Add to available data, if successful if (userContext != null) @@ -453,7 +410,7 @@ public class AuthenticationService { * If the authentication or re-authentication attempt fails. */ public String authenticate(Credentials credentials, String token) - throws GuacamoleException { + throws GuacamoleException { // Pull existing session if token provided GuacamoleSession existingSession; @@ -462,25 +419,72 @@ public class AuthenticationService { else existingSession = null; - // Get up-to-date AuthenticatedUser and associated UserContexts - AuthenticatedUser authenticatedUser = getAuthenticatedUser(existingSession, credentials); - List userContexts = getUserContexts(existingSession, authenticatedUser, credentials); - - // Update existing session, if it exists + AuthenticatedUser authenticatedUser; String authToken; - if (existingSession != null) { - authToken = token; - existingSession.setAuthenticatedUser(authenticatedUser); - existingSession.setUserContexts(userContexts); + + try { + + // Get up-to-date AuthenticatedUser and associated UserContexts + authenticatedUser = getAuthenticatedUser(existingSession, credentials); + List userContexts = getUserContexts(existingSession, authenticatedUser, credentials); + + // Update existing session, if it exists + if (existingSession != null) { + authToken = token; + existingSession.setAuthenticatedUser(authenticatedUser); + existingSession.setUserContexts(userContexts); + } + + // If no existing session, generate a new token/session pair + else { + authToken = authTokenGenerator.getToken(); + tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts)); + logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier()); + } + + // Report authentication success + try { + listenerService.handleEvent(new AuthenticationSuccessEvent(authenticatedUser)); + } + catch (GuacamoleException e) { + throw new GuacamoleAuthenticationProcessException("User " + + "authentication aborted by event listener.", null, e); + } + } - // If no existing session, generate a new token/session pair - else { - authToken = authTokenGenerator.getToken(); - tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts)); - logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier()); + // Log and rethrow any authentication errors + catch (GuacamoleAuthenticationProcessException e) { + + // Get request and username for sake of logging + HttpServletRequest request = credentials.getRequest(); + String username = credentials.getUsername(); + + listenerService.handleEvent(new AuthenticationFailureEvent(credentials, + e.getAuthenticationProvider(), e.getCause())); + + // Log authentication failures with associated usernames + if (username != null) { + if (logger.isWarnEnabled()) + logger.warn("Authentication attempt from {} for user \"{}\" failed.", + getLoggableAddress(request), username); + } + + // Log anonymous authentication failures + else if (logger.isDebugEnabled()) + logger.debug("Anonymous authentication attempt from {} failed.", + getLoggableAddress(request)); + + // Rethrow exception + throw e.getCauseAsGuacamoleException(); + } + if (logger.isInfoEnabled()) + logger.info("User \"{}\" successfully authenticated from {}.", + authenticatedUser.getIdentifier(), + getLoggableAddress(credentials.getRequest())); + return authToken; } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java index 9aa26fafc..2be283ac2 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java @@ -76,21 +76,29 @@ public class DecoratedUserContext extends DelegatingUserContext { * given AuthenticationProvider, or the original UserContext if the * given AuthenticationProvider originated the UserContext. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If the given AuthenticationProvider fails while decorating the * UserContext. */ private static UserContext decorate(AuthenticationProvider authProvider, UserContext userContext, AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { // Skip the AuthenticationProvider which produced the UserContext // being decorated if (authProvider != userContext.getAuthenticationProvider()) { // Apply layer of wrapping around UserContext - UserContext decorated = authProvider.decorate(userContext, - authenticatedUser, credentials); + UserContext decorated; + try { + decorated = authProvider.decorate(userContext, + authenticatedUser, credentials); + } + catch (GuacamoleException | RuntimeException | Error e) { + throw new GuacamoleAuthenticationProcessException("User " + + "authentication aborted by decorating UserContext.", + authProvider, e); + } // Do not allow misbehaving extensions to wipe out the // UserContext entirely @@ -130,13 +138,13 @@ public class DecoratedUserContext extends DelegatingUserContext { * given AuthenticationProvider, or the original UserContext if the * given AuthenticationProvider originated the UserContext. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If the given AuthenticationProvider fails while decorating the * UserContext. */ private static UserContext redecorate(DecoratedUserContext decorated, UserContext userContext, AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { AuthenticationProvider authProvider = decorated.getDecoratingAuthenticationProvider(); @@ -145,8 +153,16 @@ public class DecoratedUserContext extends DelegatingUserContext { if (authProvider != userContext.getAuthenticationProvider()) { // Apply next layer of wrapping around UserContext - UserContext redecorated = authProvider.redecorate(decorated.getDelegateUserContext(), - userContext, authenticatedUser, credentials); + UserContext redecorated; + try { + redecorated = authProvider.redecorate(decorated.getDelegateUserContext(), + userContext, authenticatedUser, credentials); + } + catch (GuacamoleException | RuntimeException | Error e) { + throw new GuacamoleAuthenticationProcessException("User " + + "authentication aborted by redecorating UserContext.", + authProvider, e); + } // Do not allow misbehaving extensions to wipe out the // UserContext entirely @@ -181,13 +197,13 @@ public class DecoratedUserContext extends DelegatingUserContext { * The credentials associated with the request which produced the given * UserContext. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If any of the given AuthenticationProviders fails while decorating * the UserContext. */ public DecoratedUserContext(AuthenticationProvider authProvider, UserContext userContext, AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { // Wrap the result of invoking decorate() on the given AuthenticationProvider super(decorate(authProvider, userContext, authenticatedUser, credentials)); @@ -221,13 +237,13 @@ public class DecoratedUserContext extends DelegatingUserContext { * The credentials associated with the request which produced the given * UserContext. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If any of the given AuthenticationProviders fails while decorating * the UserContext. */ public DecoratedUserContext(AuthenticationProvider authProvider, DecoratedUserContext userContext, AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { // Wrap the result of invoking decorate() on the given AuthenticationProvider super(decorate(authProvider, userContext, authenticatedUser, credentials)); @@ -261,13 +277,13 @@ public class DecoratedUserContext extends DelegatingUserContext { * The credentials associated with the request which produced the given * UserContext. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If any of the given AuthenticationProviders fails while decorating * the UserContext. */ public DecoratedUserContext(DecoratedUserContext decorated, UserContext userContext, AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { // Wrap the result of invoking redecorate() on the given AuthenticationProvider super(redecorate(decorated, userContext, authenticatedUser, credentials)); @@ -303,13 +319,13 @@ public class DecoratedUserContext extends DelegatingUserContext { * The credentials associated with the request which produced the given * UserContext. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If any of the given AuthenticationProviders fails while decorating * the UserContext. */ public DecoratedUserContext(DecoratedUserContext decorated, DecoratedUserContext userContext, AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { // Wrap the result of invoking redecorate() on the given AuthenticationProvider super(redecorate(decorated, userContext, authenticatedUser, credentials)); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java index b28dc03af..0b7fc1264 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java @@ -23,7 +23,6 @@ import java.util.Iterator; import java.util.List; import javax.inject.Inject; -import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; @@ -65,12 +64,12 @@ public class DecorationService { * A new DecoratedUserContext which has been decorated by all * AuthenticationProviders. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If any AuthenticationProvider fails while decorating the UserContext. */ public DecoratedUserContext decorate(UserContext userContext, AuthenticatedUser authenticatedUser, Credentials credentials) - throws GuacamoleException { + throws GuacamoleAuthenticationProcessException { // Get first AuthenticationProvider in list Iterator current = authProviders.iterator(); @@ -119,12 +118,12 @@ public class DecorationService { * A new DecoratedUserContext which has been decorated by all * AuthenticationProviders. * - * @throws GuacamoleException + * @throws GuacamoleAuthenticationProcessException * If any AuthenticationProvider fails while decorating the UserContext. */ public DecoratedUserContext redecorate(DecoratedUserContext decorated, UserContext userContext, AuthenticatedUser authenticatedUser, - Credentials credentials) throws GuacamoleException { + Credentials credentials) throws GuacamoleAuthenticationProcessException { // If the given DecoratedUserContext contains further decorated layers, // redecorate those first diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java new file mode 100644 index 000000000..ba1d72a50 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.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.rest.auth; + +import java.io.Serializable; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.auth.credentials.CredentialsInfo; +import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; +import org.apache.guacamole.protocol.GuacamoleStatus; + +/** + * An exception that occurs during Guacamole's authentication and authorization + * process, possibly associated with a specific AuthenticationProvider. + */ +public class GuacamoleAuthenticationProcessException extends GuacamoleException { + + /** + * Internal identifier unique to this version of + * GuacamoleAuthenticationProcessException, as required by Java's + * {@link Serializable} interface. + */ + private static final long serialVersionUID = 1L; + + /** + * The AuthenticationProvider that caused the failure, or null if there is + * no such specific AuthenticationProvider involved in this failure. + */ + private final transient AuthenticationProvider authProvider; + + /** + * A GuacamoleException representation of the failure that occurred. If + * the cause provided when this GuacamoleAuthenticationProcessException + * was created was a GuacamoleException, this will just be that exception. + * Otherwise, this will be a GuacamoleServerException wrapping the cause + * or a generic GuacamoleInvalidCredentialsException requesting a + * username/password if there is no specific cause at all. + */ + private final GuacamoleException guacCause; + + /** + * Converts the given Throwable to a GuacamoleException representing the + * failure that occurred. If the Throwable already is a GuacamoleException, + * this will just be that Throwable. For all other cases, a new + * GuacamoleException will be created that best represents the provided + * failure. If no failure is provided at all, a generic + * GuacamoleInvalidCredentialsException requesting a username/password is + * created. + * + * @param message + * A human-readable message describing the failure that occurred. + * + * @param cause + * The Throwable cause of the failure that occurred, if any, or null if + * the cause is not known to be a specific Throwable. + * + * @return + * A GuacamoleException representation of the message and cause + * provided. + */ + private static GuacamoleException toGuacamoleException(String message, + Throwable cause) { + + // Create generic invalid username/password exception if we have no + // specific cause + if (cause == null) + return new GuacamoleInvalidCredentialsException( + "Permission Denied.", + CredentialsInfo.USERNAME_PASSWORD + ); + + // If the specific cause is already a GuacamoleException, there's + // nothing for us to do here + if (cause instanceof GuacamoleException) + return (GuacamoleException) cause; + + // Wrap all other Throwables as generic internal errors + return new GuacamoleServerException(message, cause); + + } + + /** + * Creates a new GuacamoleAuthenticationProcessException with the given + * message, associated AuthenticationProvider, and cause. + * + * @param message + * A human readable description of the exception that occurred. + * + * @param authProvider + * The AuthenticationProvider that caused the failure, or null if there + * is no such specific AuthenticationProvider involved in this failure. + * + * @param cause + * The cause of this exception, or null if the cause is unknown or + * there is no such cause. + */ + public GuacamoleAuthenticationProcessException(String message, + AuthenticationProvider authProvider, Throwable cause) { + super(message, cause); + this.authProvider = authProvider; + this.guacCause = toGuacamoleException(message, cause); + } + + /** + * Returns the AuthenticationProvider that caused the failure, if any. If + * there is no specific AuthenticationProvider involved in this failure, + * including if the failure is due to multiple AuthenticationProviders, + * this will be null. + * + * @return + * The AuthenticationProvider that caused the failure, or null if there + * is no such specific AuthenticationProvider involved in this failure. + */ + public AuthenticationProvider getAuthenticationProvider() { + return authProvider; + } + + /** + * Returns a GuacamoleException that represents the user-facing cause of + * this exception. A GuacamoleException will be returned by this function + * in all cases, including if no specific cause was given. + * + * @return + * A GuacamoleException that represents the user-facing cause of this + * exception. + */ + public GuacamoleException getCauseAsGuacamoleException() { + return guacCause; + } + + @Override + public GuacamoleStatus getStatus() { + return getCauseAsGuacamoleException().getStatus(); + } + + @Override + public int getHttpStatusCode() { + return getCauseAsGuacamoleException().getHttpStatusCode(); + } + + @Override + public int getWebSocketCode() { + return getCauseAsGuacamoleException().getWebSocketCode(); + } + +} From 275b5bee134f8035acf0e00b2579d05c7405eb66 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 5 Jan 2022 15:08:54 -0800 Subject: [PATCH 055/289] GUACAMOLE-990: Add extension for automatically blocking brute-force auth attempts. --- extensions/guacamole-auth-ban/.ratignore | 0 extensions/guacamole-auth-ban/pom.xml | 60 ++++ .../src/main/assembly/dist.xml | 54 ++++ .../auth/ban/AuthenticationFailureStatus.java | 123 ++++++++ .../ban/AuthenticationFailureTracker.java | 278 ++++++++++++++++++ .../ban/BanningAuthenticationListener.java | 81 +++++ .../ban/BanningAuthenticationProvider.java | 121 ++++++++ .../src/main/resources/guac-manifest.json | 16 + extensions/pom.xml | 1 + 9 files changed, 734 insertions(+) create mode 100644 extensions/guacamole-auth-ban/.ratignore create mode 100644 extensions/guacamole-auth-ban/pom.xml create mode 100644 extensions/guacamole-auth-ban/src/main/assembly/dist.xml create mode 100644 extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java create mode 100644 extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java create mode 100644 extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java create mode 100644 extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java create mode 100644 extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json diff --git a/extensions/guacamole-auth-ban/.ratignore b/extensions/guacamole-auth-ban/.ratignore new file mode 100644 index 000000000..e69de29bb diff --git a/extensions/guacamole-auth-ban/pom.xml b/extensions/guacamole-auth-ban/pom.xml new file mode 100644 index 000000000..2be082fc2 --- /dev/null +++ b/extensions/guacamole-auth-ban/pom.xml @@ -0,0 +1,60 @@ + + + + + 4.0.0 + org.apache.guacamole + guacamole-auth-ban + jar + 1.4.0 + guacamole-auth-ban + http://guacamole.apache.org/ + + + org.apache.guacamole + extensions + 1.4.0 + ../ + + + + + + + javax.servlet + servlet-api + 2.5 + provided + + + + + org.apache.guacamole + guacamole-ext + 1.4.0 + provided + + + + + diff --git a/extensions/guacamole-auth-ban/src/main/assembly/dist.xml b/extensions/guacamole-auth-ban/src/main/assembly/dist.xml new file mode 100644 index 000000000..d046ae699 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/assembly/dist.xml @@ -0,0 +1,54 @@ + + + + + dist + ${project.artifactId}-${project.version} + + + + tar.gz + + + + + + + + + target/licenses + + + + + target + + + *.jar + + + + + + diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java new file mode 100644 index 000000000..87bdc5515 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java @@ -0,0 +1,123 @@ +/* + * 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.ban; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * The current status of an authentication failure, including the number of + * times the failure has occurred. + */ +public class AuthenticationFailureStatus { + + /** + * The timestamp of the last authentication failure, as returned by + * System.nanoTime(). + */ + private long lastFailure; + + /** + * The number of failures that have occurred. + */ + private final AtomicInteger failureCount; + + /** + * The maximum number of failures that may occur before the user/address + * causing the failures is blocked. + */ + private final int maxAttempts; + + /** + * The amount of time that a user/address must remain blocked after they + * have reached the maximum number of failures. Unlike the value provided + * at construction time, this value is maintained in nanoseconds. + */ + private final long duration; + + /** + * Creates an AuthenticationFailureStatus that represents a single failure + * and is subject to the given restrictions. Additional failures may be + * flagged after creation with {@link #notifyFailed()}. + * + * @param maxAttempts + * The maximum number of failures that may occur before the + * user/address causing the failures is blocked. + * + * @param duration + * The amount of time, in seconds, that a user/address must remain + * blocked after they have reached the maximum number of failures. + */ + public AuthenticationFailureStatus(int maxAttempts, int duration) { + this.lastFailure = System.nanoTime(); + this.failureCount = new AtomicInteger(1); + this.maxAttempts = maxAttempts; + this.duration = TimeUnit.SECONDS.toNanos(duration); + } + + /** + * Updates this authentication failure, noting that the failure it + * represents has recurred. + */ + public void notifyFailed() { + lastFailure = System.nanoTime(); + failureCount.incrementAndGet(); + } + + /** + * Returns whether this authentication failure is recent enough that it + * should still be tracked. This function will return false for + * authentication failures that have not recurred for at least the duration + * provided at construction time. + * + * @return + * true if this authentication failure is recent enough that it should + * still be tracked, false otherwise. + */ + public boolean isValid() { + return System.nanoTime() - lastFailure <= duration; + } + + /** + * Returns whether the user/address causing this authentication failure + * should be blocked based on the restrictions provided at construction + * time. + * + * @return + * true if the user/address causing this failure should be blocked, + * false otherwise. + */ + public boolean isBlocked() { + return isValid() && failureCount.get() >= maxAttempts; + } + + /** + * Returns the total number of authentication failures that have been + * recorded through creating this object and invoking + * {@link #notifyFailed()}. + * + * @return + * The total number of failures that have occurred. + */ + public int getFailures() { + return failureCount.get(); + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java new file mode 100644 index 000000000..4f77875fc --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java @@ -0,0 +1,278 @@ +/* + * 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.ban; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.servlet.http.HttpServletRequest; +import org.apache.guacamole.GuacamoleClientTooManyException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.net.auth.Credentials; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides automated tracking and blocking of IP addresses that repeatedly + * fail authentication. + */ +public class AuthenticationFailureTracker { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(AuthenticationFailureTracker.class); + + /** + * All authentication failures currently being tracked, stored by the + * associated IP address. + */ + private final ConcurrentMap failures = + new ConcurrentHashMap<>(); + + /** + * The maximum number of failed authentication attempts allowed before an + * address is temporarily banned. + */ + private final int maxAttempts; + + /** + * The length of time that each address should be banned after reaching the + * maximum number of failed authentication attempts, in seconds. + */ + private final int banDuration; + + /** + * Creates a new AuthenticationFailureTracker that automatically blocks + * authentication attempts based on the provided blocking criteria. + * + * @param maxAttempts + * The maximum number of failed authentication attempts allowed before + * an address is temporarily banned. + * + * @param banDuration + * The length of time that each address should be banned after reaching + * the maximum number of failed authentication attempts, in seconds. + */ + public AuthenticationFailureTracker(int maxAttempts, int banDuration) { + this.maxAttempts = maxAttempts; + this.banDuration = banDuration; + + // Inform administrator of configured behavior + if (maxAttempts <= 0) { + logger.info("Maximum failed authentication attempts has been set " + + "to {}. Automatic banning of brute-force authentication " + + "attempts will be disabled.", maxAttempts); + } + else if (banDuration <= 0) { + logger.info("Ban duration for addresses that repeatedly fail " + + "authentication has been set to {}. Automatic banning " + + "of brute-force authentication attempts will be " + + "disabled.", banDuration); + } + else { + logger.info("Addresses will be automatically banned for {} " + + "seconds after {} failed authentication attempts.", + banDuration, maxAttempts); + } + + } + + /** + * Returns whether the given Credentials do not contain any specific + * authentication parameters, including HTTP parameters. An authentication + * request that contains no parameters whatsoever will tend to be the + * first, anonymous, credential-less authentication attempt that results in + * the initial login screen rendering. + * + * @param credentials + * The Credentials object to test. + * + * @return + * true if the given Credentials contain no authentication parameters + * whatsoever, false otherwise. + */ + private boolean isEmpty(Credentials credentials) { + + // An authentication request that contains an explicit username or + // password (even if blank) is non-empty, regardless of how the values + // were passed + if (credentials.getUsername() != null || credentials.getPassword() != null) + return false; + + // All further tests depend on HTTP request details + HttpServletRequest request = credentials.getRequest(); + if (request == null) + return true; + + // An authentication request is non-empty if it contains any HTTP + // parameters at all or contains an authentication token + return !request.getParameterNames().hasMoreElements() + && request.getHeader("Guacamole-Token") == null; + + } + + /** + * Reports that the given address has just failed to authenticate and + * returns the AuthenticationFailureStatus that represents that failure. If + * the address isn't already being tracked, it will begin being tracked as + * of this call. If the address is already tracked, the returned + * AuthenticationFailureStatus will represent past authentication failures, + * as well. + * + * @param address + * The address that has failed to authenticate. + * + * @return + * An AuthenticationFailureStatus that represents this latest + * authentication failure for the given address, as well as any past + * failures. + */ + private AuthenticationFailureStatus getAuthenticationFailure(String address) { + + AuthenticationFailureStatus newFailure = new AuthenticationFailureStatus(maxAttempts, banDuration); + AuthenticationFailureStatus status = failures.putIfAbsent(address, newFailure); + if (status == null) + return newFailure; + + status.notifyFailed(); + return status; + + } + + /** + * Reports that an authentication request has been received, as well as + * whether that request is known to have failed. If the associated address + * is currently being blocked, an exception will be thrown. + * + * @param credentials + * The credentials associated with the authentication request. + * + * @param failed + * Whether the request is known to have failed. If the status of the + * request is not yet known, this should be false. + * + * @throws GuacamoleException + * If the authentication request is being blocked due to brute force + * prevention rules. + */ + private void notifyAuthenticationStatus(Credentials credentials, + boolean failed) throws GuacamoleException { + + // Do not track/ban if tracking or banning are disabled + if (maxAttempts <= 0 || banDuration <= 0) + return; + + // Ignore requests that do not contain explicit parameters of any kind + if (isEmpty(credentials)) + return; + + // Determine originating address of the authentication request + String address = credentials.getRemoteAddress(); + if (address == null) + throw new GuacamoleServerException("Source address cannot be determined."); + + // Get current failure status for the address associated with the + // authentication request, adding/updating that status if the request + // was itself a failure + AuthenticationFailureStatus status; + if (failed) { + status = getAuthenticationFailure(address); + logger.debug("Authentication has failed for address \"{}\" (current total failures: {}/{}).", + address, status.getFailures(), maxAttempts); + } + else + status = failures.get(address); + + if (status != null) { + + // Explicitly block further processing of authentication/authorization + // if too many failures have occurred + if (status.isBlocked()) { + logger.debug("Blocking authentication attempt from address \"{}\" due to number of authentication failures.", address); + throw new GuacamoleClientTooManyException("Too many failed " + + "authentication attempts. Please try again later."); + } + + // Clean up tracking of failures if the address is no longer + // relevant (all failures are sufficiently old) + else if (!status.isValid()) { + logger.debug("Removing address \"{}\" from tracking as there are no recent authentication failures.", address); + failures.remove(address); + } + + } + + } + + /** + * Reports that an authentication request has been received, but it is + * either not yet known whether the request has succeeded or failed. If the + * associated address is currently being blocked, an exception will be + * thrown. + * + * @param credentials + * The credentials associated with the authentication request. + * + * @throws GuacamoleException + * If the authentication request is being blocked due to brute force + * prevention rules. + */ + public void notifyAuthenticationRequestReceived(Credentials credentials) + throws GuacamoleException { + notifyAuthenticationStatus(credentials, false); + } + + /** + * Reports that an authentication request has been received and has + * succeeded. If the associated address is currently being blocked, an + * exception will be thrown. + * + * @param credentials + * The credentials associated with the successful authentication + * request. + * + * @throws GuacamoleException + * If the authentication request is being blocked due to brute force + * prevention rules. + */ + public void notifyAuthenticationSuccess(Credentials credentials) + throws GuacamoleException { + notifyAuthenticationStatus(credentials, false); + } + + /** + * Reports that an authentication request has been received and has + * failed. If the associated address is currently being blocked, an + * exception will be thrown. + * + * @param credentials + * The credentials associated with the failed authentication request. + * + * @throws GuacamoleException + * If the authentication request is being blocked due to brute force + * prevention rules. + */ + public void notifyAuthenticationFailed(Credentials credentials) + throws GuacamoleException { + notifyAuthenticationStatus(credentials, true); + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java new file mode 100644 index 000000000..38fd575fd --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java @@ -0,0 +1,81 @@ +/* + * 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.ban; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; +import org.apache.guacamole.net.event.AuthenticationFailureEvent; +import org.apache.guacamole.net.event.AuthenticationSuccessEvent; +import org.apache.guacamole.net.event.listener.Listener; + +/** + * Listener implementation which automatically tracks authentication failures + * such that further authentication attempts may be automatically blocked by + * {@link BanningAuthenticationProvider} if they match configured criteria. + */ +public class BanningAuthenticationListener implements Listener { + + /** + * Shared tracker of addresses that have repeatedly failed authentication. + */ + private static AuthenticationFailureTracker tracker; + + /** + * Assigns the shared tracker instance used by both the {@link BanningAuthenticationProvider} + * and this listener. This function MUST be invoked with the tracker + * created for BanningAuthenticationProvider as soon as possible (during + * construction of BanningAuthenticationProvider), or processing of + * received events will fail internally. + * + * @param tracker + * The tracker instance to use for received authentication events. + */ + public static void setAuthenticationFailureTracker(AuthenticationFailureTracker tracker) { + BanningAuthenticationListener.tracker = tracker; + } + + @Override + public void handleEvent(Object event) throws GuacamoleException { + + if (event instanceof AuthenticationFailureEvent) { + + AuthenticationFailureEvent failure = (AuthenticationFailureEvent) event; + + // Requests for additional credentials are not failures per se, + // but continuations of a multi-request authentication attempt that + // has not yet succeeded OR failed + if (failure.getFailure() instanceof GuacamoleInsufficientCredentialsException) { + tracker.notifyAuthenticationRequestReceived(failure.getCredentials()); + return; + } + + // Consider all other errors to be failed auth attempts + tracker.notifyAuthenticationFailed(failure.getCredentials()); + + } + + else if (event instanceof AuthenticationSuccessEvent) { + AuthenticationSuccessEvent success = (AuthenticationSuccessEvent) event; + tracker.notifyAuthenticationSuccess(success.getCredentials()); + } + + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java new file mode 100644 index 000000000..12195f5e4 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java @@ -0,0 +1,121 @@ +/* + * 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.ban; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.environment.LocalEnvironment; +import org.apache.guacamole.net.auth.AbstractAuthenticationProvider; +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.properties.IntegerGuacamoleProperty; + +/** + * AuthenticationProvider implementation that blocks further authentication + * attempts that are related to past authentication failures flagged by + * {@link BanningAuthenticationListener}. + */ +public class BanningAuthenticationProvider extends AbstractAuthenticationProvider { + + /** + * The maximum number of failed authentication attempts allowed before an + * address is temporarily banned. + */ + private static final IntegerGuacamoleProperty MAX_ATTEMPTS = new IntegerGuacamoleProperty() { + + @Override + public String getName() { + return "ban-max-invalid-attempts"; + } + + }; + + /** + * The length of time that each address should be banned after reaching the + * maximum number of failed authentication attempts, in seconds. + */ + private static final IntegerGuacamoleProperty IP_BAN_DURATION = new IntegerGuacamoleProperty() { + + @Override + public String getName() { + return "ban-address-duration"; + } + + }; + + /** + * The default maximum number of failed authentication attempts allowed + * before an address is temporarily banned. + */ + private static final int DEFAULT_MAX_ATTEMPTS = 5; + + /** + * The default length of time that each address should be banned after + * reaching the maximum number of failed authentication attempts, in + * seconds. + */ + private static final int DEFAULT_IP_BAN_DURATION = 300; + + /** + * Shared tracker of addresses that have repeatedly failed authentication. + */ + private final AuthenticationFailureTracker tracker; + + /** + * Creates a new BanningAuthenticationProvider which automatically bans + * further authentication attempts from addresses that have repeatedly + * failed to authenticate. The ban duration and maximum number of failed + * attempts allowed before banning are configured within + * guacamole.properties. + * + * @throws GuacamoleException + * If an error occurs parsing the configuration properties used by this + * extension. + */ + public BanningAuthenticationProvider() throws GuacamoleException { + + Environment environment = LocalEnvironment.getInstance(); + int maxAttempts = environment.getProperty(MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS); + int banDuration = environment.getProperty(IP_BAN_DURATION, DEFAULT_IP_BAN_DURATION); + + tracker = new AuthenticationFailureTracker(maxAttempts, banDuration); + BanningAuthenticationListener.setAuthenticationFailureTracker(tracker); + + } + + @Override + public String getIdentifier() { + return "ban"; + } + + @Override + public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { + tracker.notifyAuthenticationRequestReceived(credentials); + return null; + } + + @Override + public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { + tracker.notifyAuthenticationRequestReceived(authenticatedUser.getCredentials()); + return null; + } + +} diff --git a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..87f75ea61 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json @@ -0,0 +1,16 @@ +{ + + "guacamoleVersion" : "1.4.0", + + "name" : "Brute-force Authentication Detection/Prevention", + "namespace" : "ban", + + "authProviders" : [ + "org.apache.guacamole.auth.ban.BanningAuthenticationProvider" + ], + + "listeners" : [ + "org.apache.guacamole.auth.ban.BanningAuthenticationListener" + ] + +} diff --git a/extensions/pom.xml b/extensions/pom.xml index 3bab33257..b16b3ed53 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -40,6 +40,7 @@ + guacamole-auth-ban guacamole-auth-duo guacamole-auth-header guacamole-auth-jdbc From ffb023c6c3c387b8dd1489bf72917b63fc64d3b1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 10:14:55 -0700 Subject: [PATCH 056/289] GUACAMOLE-1614: Include chromium-driver in Docker build for sake of JS unit tests. --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 5af326bb3..5bafc1972 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,9 @@ ARG TOMCAT_JRE=jdk8 # Use official maven image for the build FROM maven:3-jdk-8 AS builder +# Install chromium-driver for sake of JavaScript unit tests +RUN apt-get update && apt-get install -y chromium-driver + # Use args to build radius auth extension such as # `--build-arg BUILD_PROFILE=lgpl-extensions` ARG BUILD_PROFILE From f9d8abcfdeb50c8e8630aafb3fc19ff95be42670 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 09:17:29 -0700 Subject: [PATCH 057/289] GUACAMOLE-990: Clear out any previous authentication token that is known to be invalid. --- .../src/app/auth/service/authenticationService.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index 944fd4a9a..199d6cb2e 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -198,12 +198,16 @@ angular.module('auth').factory('authenticationService', ['$injector', ['catch'](requestService.createErrorCallback(function authenticationFailed(error) { // Request credentials if provided credentials were invalid - if (error.type === Error.Type.INVALID_CREDENTIALS) + if (error.type === Error.Type.INVALID_CREDENTIALS) { $rootScope.$broadcast('guacInvalidCredentials', parameters, error); + clearAuthenticationResult(); + } // Request more credentials if provided credentials were not enough - else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) + else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) { $rootScope.$broadcast('guacInsufficientCredentials', parameters, error); + clearAuthenticationResult(); + } // Abort rendering of page if an internal error occurs else if (error.type === Error.Type.INTERNAL_ERROR) From 2b19bc95daeead7127ddcc9b65260c2794d18fc6 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 09:40:10 -0700 Subject: [PATCH 058/289] GUACAMOLE-990: Use translation string for "too many failed attempts" error. --- .../guacamole/auth/ban/AuthenticationFailureTracker.java | 7 ++++--- .../src/main/resources/guac-manifest.json | 4 ++++ .../src/main/resources/translations/en.json | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 extensions/guacamole-auth-ban/src/main/resources/translations/en.json diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java index 4f77875fc..d30f522bd 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java @@ -22,9 +22,9 @@ package org.apache.guacamole.auth.ban; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.servlet.http.HttpServletRequest; -import org.apache.guacamole.GuacamoleClientTooManyException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.language.TranslatableGuacamoleClientTooManyException; import org.apache.guacamole.net.auth.Credentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -207,8 +207,9 @@ public class AuthenticationFailureTracker { // if too many failures have occurred if (status.isBlocked()) { logger.debug("Blocking authentication attempt from address \"{}\" due to number of authentication failures.", address); - throw new GuacamoleClientTooManyException("Too many failed " - + "authentication attempts. Please try again later."); + throw new TranslatableGuacamoleClientTooManyException("Too " + + "many failed authentication attempts.", + "LOGIN.ERROR_TOO_MANY_ATTEMPTS"); } // Clean up tracking of failures if the address is no longer diff --git a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json index 87f75ea61..1e6beac22 100644 --- a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json @@ -11,6 +11,10 @@ "listeners" : [ "org.apache.guacamole.auth.ban.BanningAuthenticationListener" + ], + + "translations" : [ + "translations/en.json" ] } diff --git a/extensions/guacamole-auth-ban/src/main/resources/translations/en.json b/extensions/guacamole-auth-ban/src/main/resources/translations/en.json new file mode 100644 index 000000000..2ef8a37d7 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/resources/translations/en.json @@ -0,0 +1,5 @@ +{ + "LOGIN": { + "ERROR_TOO_MANY_ATTEMPTS" : "Too many failed authentication attempts. Please try again later." + } +} From 43f65357c8b0de44db925c66716ff0ad668c3af8 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 10:40:52 -0700 Subject: [PATCH 059/289] GUACAMOLE-990: Limit maximum number of tracked addresses. --- doc/licenses/caffeine-2.9.3/README | 8 +++++ .../caffeine-2.9.3/dep-coordinates.txt | 1 + doc/licenses/checker-qual-3.19.0/LICENSE.txt | 22 +++++++++++++ doc/licenses/checker-qual-3.19.0/README | 8 +++++ .../checker-qual-3.19.0/dep-coordinates.txt | 1 + doc/licenses/error-prone-2.10.0/README | 8 +++++ .../error-prone-2.10.0/dep-coordinates.txt | 1 + extensions/guacamole-auth-ban/pom.xml | 21 ++++++++++++ .../auth/ban/AuthenticationFailureStatus.java | 8 ++--- .../ban/AuthenticationFailureTracker.java | 33 ++++++++++++------- .../ban/BanningAuthenticationProvider.java | 32 +++++++++++++++++- 11 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 doc/licenses/caffeine-2.9.3/README create mode 100644 doc/licenses/caffeine-2.9.3/dep-coordinates.txt create mode 100644 doc/licenses/checker-qual-3.19.0/LICENSE.txt create mode 100644 doc/licenses/checker-qual-3.19.0/README create mode 100644 doc/licenses/checker-qual-3.19.0/dep-coordinates.txt create mode 100644 doc/licenses/error-prone-2.10.0/README create mode 100644 doc/licenses/error-prone-2.10.0/dep-coordinates.txt diff --git a/doc/licenses/caffeine-2.9.3/README b/doc/licenses/caffeine-2.9.3/README new file mode 100644 index 000000000..c51ba745c --- /dev/null +++ b/doc/licenses/caffeine-2.9.3/README @@ -0,0 +1,8 @@ +Caffeine (https://github.com/ben-manes/caffeine) +------------------------------------------------ + + Version: 2.9.3 + From: 'Ben Manes' (https://github.com/ben-manes) + License(s): + Apache v2.0 + diff --git a/doc/licenses/caffeine-2.9.3/dep-coordinates.txt b/doc/licenses/caffeine-2.9.3/dep-coordinates.txt new file mode 100644 index 000000000..feda8aa63 --- /dev/null +++ b/doc/licenses/caffeine-2.9.3/dep-coordinates.txt @@ -0,0 +1 @@ +com.github.ben-manes.caffeine:caffeine:jar:2.9.3 diff --git a/doc/licenses/checker-qual-3.19.0/LICENSE.txt b/doc/licenses/checker-qual-3.19.0/LICENSE.txt new file mode 100644 index 000000000..9837c6b69 --- /dev/null +++ b/doc/licenses/checker-qual-3.19.0/LICENSE.txt @@ -0,0 +1,22 @@ +Checker Framework qualifiers +Copyright 2004-present by the Checker Framework developers + +MIT License: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/doc/licenses/checker-qual-3.19.0/README b/doc/licenses/checker-qual-3.19.0/README new file mode 100644 index 000000000..4a00db7a7 --- /dev/null +++ b/doc/licenses/checker-qual-3.19.0/README @@ -0,0 +1,8 @@ +Checker Framework qualifiers (https://checkerframework.org/) +------------------------------------------------------------ + + Version: 3.19.0 + From: 'Checker Framework developers' (https://checkerframework.org/) + License(s): + MIT (bundled/checker-qual-3.19.0/LICENSE.txt) + diff --git a/doc/licenses/checker-qual-3.19.0/dep-coordinates.txt b/doc/licenses/checker-qual-3.19.0/dep-coordinates.txt new file mode 100644 index 000000000..322f3959d --- /dev/null +++ b/doc/licenses/checker-qual-3.19.0/dep-coordinates.txt @@ -0,0 +1 @@ +org.checkerframework:checker-qual:jar:3.19.0 diff --git a/doc/licenses/error-prone-2.10.0/README b/doc/licenses/error-prone-2.10.0/README new file mode 100644 index 000000000..f286aed3b --- /dev/null +++ b/doc/licenses/error-prone-2.10.0/README @@ -0,0 +1,8 @@ +Error Prone (https://errorprone.info/) +-------------------------------------- + + Version: 2.10.0 + From: 'Google Inc.' (http://www.google.com/) + License(s): + Apache v2.0 + diff --git a/doc/licenses/error-prone-2.10.0/dep-coordinates.txt b/doc/licenses/error-prone-2.10.0/dep-coordinates.txt new file mode 100644 index 000000000..9473dfcab --- /dev/null +++ b/doc/licenses/error-prone-2.10.0/dep-coordinates.txt @@ -0,0 +1 @@ +com.google.errorprone:error_prone_annotations:jar:2.10.0 diff --git a/extensions/guacamole-auth-ban/pom.xml b/extensions/guacamole-auth-ban/pom.xml index 2be082fc2..68b8d6f90 100644 --- a/extensions/guacamole-auth-ban/pom.xml +++ b/extensions/guacamole-auth-ban/pom.xml @@ -53,6 +53,27 @@ guacamole-ext 1.4.0 provided + + + + + org.checkerframework + checker-qual + + + com.google.errorprone + error_prone_annotations + + + + + + + + com.github.ben-manes.caffeine + caffeine + 2.9.3 diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java index 87bdc5515..ac4f1efc1 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java @@ -53,9 +53,9 @@ public class AuthenticationFailureStatus { private final long duration; /** - * Creates an AuthenticationFailureStatus that represents a single failure - * and is subject to the given restrictions. Additional failures may be - * flagged after creation with {@link #notifyFailed()}. + * Creates an AuthenticationFailureStatus that is initialized to zero + * failures and is subject to the given restrictions. Additional failures + * may be flagged after creation with {@link #notifyFailed()}. * * @param maxAttempts * The maximum number of failures that may occur before the @@ -67,7 +67,7 @@ public class AuthenticationFailureStatus { */ public AuthenticationFailureStatus(int maxAttempts, int duration) { this.lastFailure = System.nanoTime(); - this.failureCount = new AtomicInteger(1); + this.failureCount = new AtomicInteger(0); this.maxAttempts = maxAttempts; this.duration = TimeUnit.SECONDS.toNanos(duration); } diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java index d30f522bd..85f5cc09c 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java @@ -19,8 +19,8 @@ package org.apache.guacamole.auth.ban; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; @@ -44,8 +44,7 @@ public class AuthenticationFailureTracker { * All authentication failures currently being tracked, stored by the * associated IP address. */ - private final ConcurrentMap failures = - new ConcurrentHashMap<>(); + private final Cache failures; /** * The maximum number of failed authentication attempts allowed before an @@ -70,8 +69,14 @@ public class AuthenticationFailureTracker { * @param banDuration * The length of time that each address should be banned after reaching * the maximum number of failed authentication attempts, in seconds. + * + * @param maxAddresses + * The maximum number of unique IP addresses that should be tracked + * before discarding older tracked failures. */ - public AuthenticationFailureTracker(int maxAttempts, int banDuration) { + public AuthenticationFailureTracker(int maxAttempts, int banDuration, + long maxAddresses) { + this.maxAttempts = maxAttempts; this.banDuration = banDuration; @@ -93,6 +98,14 @@ public class AuthenticationFailureTracker { banDuration, maxAttempts); } + // Limit maximum number of tracked addresses to configured upper bound + this.failures = Caffeine.newBuilder() + .maximumSize(maxAddresses) + .build(); + + logger.info("Up to {} unique addresses will be tracked/banned at any " + + " given time.", maxAddresses); + } /** @@ -147,10 +160,8 @@ public class AuthenticationFailureTracker { */ private AuthenticationFailureStatus getAuthenticationFailure(String address) { - AuthenticationFailureStatus newFailure = new AuthenticationFailureStatus(maxAttempts, banDuration); - AuthenticationFailureStatus status = failures.putIfAbsent(address, newFailure); - if (status == null) - return newFailure; + AuthenticationFailureStatus status = failures.get(address, + (addr) -> new AuthenticationFailureStatus(maxAttempts, banDuration)); status.notifyFailed(); return status; @@ -199,7 +210,7 @@ public class AuthenticationFailureTracker { address, status.getFailures(), maxAttempts); } else - status = failures.get(address); + status = failures.getIfPresent(address); if (status != null) { @@ -216,7 +227,7 @@ public class AuthenticationFailureTracker { // relevant (all failures are sufficiently old) else if (!status.isValid()) { logger.debug("Removing address \"{}\" from tracking as there are no recent authentication failures.", address); - failures.remove(address); + failures.invalidate(address); } } diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java index 12195f5e4..adf54ff0c 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java @@ -20,6 +20,7 @@ package org.apache.guacamole.auth.ban; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.net.auth.AbstractAuthenticationProvider; @@ -27,6 +28,7 @@ import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.properties.IntegerGuacamoleProperty; +import org.apache.guacamole.properties.LongGuacamoleProperty; /** * AuthenticationProvider implementation that blocks further authentication @@ -61,6 +63,20 @@ public class BanningAuthenticationProvider extends AbstractAuthenticationProvide }; + /** + * The maximum number of failed authentication attempts tracked at any + * given time. Once this number of addresses is exceeded, the oldest + * authentication attempts are rotated off on an LRU basis. + */ + private static final LongGuacamoleProperty MAX_ADDRESSES = new LongGuacamoleProperty() { + + @Override + public String getName() { + return "ban-max-addresses"; + } + + }; + /** * The default maximum number of failed authentication attempts allowed * before an address is temporarily banned. @@ -74,6 +90,13 @@ public class BanningAuthenticationProvider extends AbstractAuthenticationProvide */ private static final int DEFAULT_IP_BAN_DURATION = 300; + /** + * The maximum number of failed authentication attempts tracked at any + * given time. Once this number of addresses is exceeded, the oldest + * authentication attempts are rotated off on an LRU basis. + */ + private static final long DEFAULT_MAX_ADDRESSES = 10485760; + /** * Shared tracker of addresses that have repeatedly failed authentication. */ @@ -95,8 +118,15 @@ public class BanningAuthenticationProvider extends AbstractAuthenticationProvide Environment environment = LocalEnvironment.getInstance(); int maxAttempts = environment.getProperty(MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS); int banDuration = environment.getProperty(IP_BAN_DURATION, DEFAULT_IP_BAN_DURATION); + long maxAddresses = environment.getProperty(MAX_ADDRESSES, DEFAULT_MAX_ADDRESSES); - tracker = new AuthenticationFailureTracker(maxAttempts, banDuration); + if (maxAddresses <= 0) + throw new GuacamoleServerException("The maximum number of " + + "addresses tracked, as specified by the " + + "\"" + MAX_ADDRESSES.getName() + "\" property, must be " + + "greater than zero."); + + tracker = new AuthenticationFailureTracker(maxAttempts, banDuration, maxAddresses); BanningAuthenticationListener.setAuthenticationFailureTracker(tracker); } From 8b981d92135253626239dd957192747eb6532c0f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 12:13:19 -0700 Subject: [PATCH 060/289] GUACAMOLE-990: Add Docker image support for guacamole-auth-ban. --- guacamole-docker/bin/build-guacamole.sh | 10 ++++++++++ guacamole-docker/bin/start.sh | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/guacamole-docker/bin/build-guacamole.sh b/guacamole-docker/bin/build-guacamole.sh index 9f1773d83..6ffc86611 100755 --- a/guacamole-docker/bin/build-guacamole.sh +++ b/guacamole-docker/bin/build-guacamole.sh @@ -198,3 +198,13 @@ if [ -f extensions/guacamole-auth-json/target/guacamole-auth-json*.jar ]; then mkdir -p "$DESTINATION/json" cp extensions/guacamole-auth-json/target/guacamole-auth-json*.jar "$DESTINATION/json" fi + +# +# Copy automatic brute-force banning auth extension if it was built +# + +if [ -f extensions/guacamole-auth-ban/target/guacamole-auth-ban*.jar ]; then + mkdir -p "$DESTINATION/ban" + cp extensions/guacamole-auth-ban/target/guacamole-auth-ban*.jar "$DESTINATION/ban" +fi + diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh index c9d205b58..632c3d03a 100755 --- a/guacamole-docker/bin/start.sh +++ b/guacamole-docker/bin/start.sh @@ -1160,6 +1160,18 @@ if [ -n "$API_SESSION_TIMEOUT" ]; then associate_apisessiontimeout fi +# Apply any overrides for default address ban behavior +set_optional_property "ban-address-duration" "$BAN_ADDRESS_DURATION" +set_optional_property "ban-max-addresses" "$BAN_MAX_ADDRESSES" +set_optional_property "ban-max-invalid-attempts" "$BAN_MAX_INVALID_ATTEMPTS" + +# Ensure guacamole-auth-ban always loads before other extensions unless +# explicitly overridden via naming or EXTENSION_PRIORITY (allowing other +# extensions to attempt authentication before guacamole-auth-ban has a chance +# to enforce any bans could allow credentials to continue to be guessed even +# after the address has been blocked via timing attacks) +ln -s /opt/guacamole/ban/guacamole-auth-*.jar "$GUACAMOLE_EXT/_guacamole-auth-ban.jar" + # Set logback level if specified if [ -n "$LOGBACK_LEVEL" ]; then unzip -o -j /opt/guacamole/guacamole.war WEB-INF/classes/logback.xml -d $GUACAMOLE_HOME From a9ed4c298206fb14503811151fab0d03ad1d570e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 12:19:52 -0700 Subject: [PATCH 061/289] GUACAMOLE-990: Revise guacamole-auth-ban log levels to generally always notify of problematic addresses. --- .../guacamole/auth/ban/AuthenticationFailureTracker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java index 85f5cc09c..72b4fc8dd 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java @@ -206,7 +206,7 @@ public class AuthenticationFailureTracker { AuthenticationFailureStatus status; if (failed) { status = getAuthenticationFailure(address); - logger.debug("Authentication has failed for address \"{}\" (current total failures: {}/{}).", + logger.info("Authentication has failed for address \"{}\" (current total failures: {}/{}).", address, status.getFailures(), maxAttempts); } else @@ -217,7 +217,7 @@ public class AuthenticationFailureTracker { // Explicitly block further processing of authentication/authorization // if too many failures have occurred if (status.isBlocked()) { - logger.debug("Blocking authentication attempt from address \"{}\" due to number of authentication failures.", address); + logger.warn("Blocking authentication attempt from address \"{}\" due to number of authentication failures.", address); throw new TranslatableGuacamoleClientTooManyException("Too " + "many failed authentication attempts.", "LOGIN.ERROR_TOO_MANY_ATTEMPTS"); From 584db45a4fdbb950ec4e793e5ee958f9d3fcff36 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 15:08:42 -0700 Subject: [PATCH 062/289] GUACAMOLE-990: Enabled/disable auth failure tracking via implementations of a common interface. --- .../ban/BanningAuthenticationListener.java | 1 + .../ban/BanningAuthenticationProvider.java | 34 +++++++- .../AuthenticationFailureStatus.java | 2 +- .../status/AuthenticationFailureTracker.java | 78 ++++++++++++++++++ ...InMemoryAuthenticationFailureTracker.java} | 79 +++---------------- .../NullAuthenticationFailureTracker.java | 49 ++++++++++++ 6 files changed, 172 insertions(+), 71 deletions(-) rename extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/{ => status}/AuthenticationFailureStatus.java (98%) create mode 100644 extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureTracker.java rename extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/{AuthenticationFailureTracker.java => status/InMemoryAuthenticationFailureTracker.java} (74%) create mode 100644 extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/NullAuthenticationFailureTracker.java diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java index 38fd575fd..4d8a3bbad 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java @@ -19,6 +19,7 @@ package org.apache.guacamole.auth.ban; +import org.apache.guacamole.auth.ban.status.AuthenticationFailureTracker; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.event.AuthenticationFailureEvent; diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java index adf54ff0c..a6df43a30 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java @@ -19,8 +19,11 @@ package org.apache.guacamole.auth.ban; +import org.apache.guacamole.auth.ban.status.InMemoryAuthenticationFailureTracker; +import org.apache.guacamole.auth.ban.status.AuthenticationFailureTracker; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.ban.status.NullAuthenticationFailureTracker; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.net.auth.AbstractAuthenticationProvider; @@ -29,6 +32,8 @@ import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.LongGuacamoleProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * AuthenticationProvider implementation that blocks further authentication @@ -37,6 +42,11 @@ import org.apache.guacamole.properties.LongGuacamoleProperty; */ public class BanningAuthenticationProvider extends AbstractAuthenticationProvider { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(BanningAuthenticationProvider.class); + /** * The maximum number of failed authentication attempts allowed before an * address is temporarily banned. @@ -126,7 +136,29 @@ public class BanningAuthenticationProvider extends AbstractAuthenticationProvide + "\"" + MAX_ADDRESSES.getName() + "\" property, must be " + "greater than zero."); - tracker = new AuthenticationFailureTracker(maxAttempts, banDuration, maxAddresses); + // Configure auth failure tracking behavior and inform administrator of + // ultimate result + if (maxAttempts <= 0) { + this.tracker = new NullAuthenticationFailureTracker(); + logger.info("Maximum failed authentication attempts has been set " + + "to {}. Automatic banning of brute-force authentication " + + "attempts will be disabled.", maxAttempts); + } + else if (banDuration <= 0) { + this.tracker = new NullAuthenticationFailureTracker(); + logger.info("Ban duration for addresses that repeatedly fail " + + "authentication has been set to {}. Automatic banning " + + "of brute-force authentication attempts will be " + + "disabled.", banDuration); + } + else { + this.tracker = new InMemoryAuthenticationFailureTracker(maxAttempts, banDuration, maxAddresses); + logger.info("Addresses will be automatically banned for {} " + + "seconds after {} failed authentication attempts. Up " + + "to {} unique addresses will be tracked/banned at any " + + "given time.", banDuration, maxAttempts, maxAddresses); + } + BanningAuthenticationListener.setAuthenticationFailureTracker(tracker); } diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureStatus.java similarity index 98% rename from extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java rename to extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureStatus.java index ac4f1efc1..3292d1179 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureStatus.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.guacamole.auth.ban; +package org.apache.guacamole.auth.ban.status; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureTracker.java new file mode 100644 index 000000000..9ea7f2c9e --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureTracker.java @@ -0,0 +1,78 @@ +/* + * 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.ban.status; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.Credentials; + +/** + * Tracks past authentication results, automatically blocking the IP addresses + * of machines that repeatedly fail to authenticate. + */ +public interface AuthenticationFailureTracker { + + /** + * Reports that an authentication request has been received, but it is + * either not yet known whether the request has succeeded or failed. If the + * associated address is currently being blocked, an exception will be + * thrown. + * + * @param credentials + * The credentials associated with the authentication request. + * + * @throws GuacamoleException + * If the authentication request is being blocked due to brute force + * prevention rules. + */ + void notifyAuthenticationRequestReceived(Credentials credentials) + throws GuacamoleException; + + /** + * Reports that an authentication request has been received and has + * succeeded. If the associated address is currently being blocked, an + * exception will be thrown. + * + * @param credentials + * The credentials associated with the successful authentication + * request. + * + * @throws GuacamoleException + * If the authentication request is being blocked due to brute force + * prevention rules. + */ + void notifyAuthenticationSuccess(Credentials credentials) + throws GuacamoleException; + + /** + * Reports that an authentication request has been received and has + * failed. If the associated address is currently being blocked, an + * exception will be thrown. + * + * @param credentials + * The credentials associated with the failed authentication request. + * + * @throws GuacamoleException + * If the authentication request is being blocked due to brute force + * prevention rules. + */ + void notifyAuthenticationFailed(Credentials credentials) + throws GuacamoleException; + +} diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java similarity index 74% rename from extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java rename to extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java index 72b4fc8dd..b655168e1 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.guacamole.auth.ban; +package org.apache.guacamole.auth.ban.status; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; @@ -30,15 +30,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Provides automated tracking and blocking of IP addresses that repeatedly - * fail authentication. + * AuthenticationFailureTracker implementation that tracks the failure status + * of each IP address in memory. The maximum amount of memory consumed is + * bounded by the configured maximum number of addresses tracked. */ -public class AuthenticationFailureTracker { +public class InMemoryAuthenticationFailureTracker implements AuthenticationFailureTracker { /** * Logger for this class. */ - private static final Logger logger = LoggerFactory.getLogger(AuthenticationFailureTracker.class); + private static final Logger logger = LoggerFactory.getLogger(InMemoryAuthenticationFailureTracker.class); /** * All authentication failures currently being tracked, stored by the @@ -74,38 +75,17 @@ public class AuthenticationFailureTracker { * The maximum number of unique IP addresses that should be tracked * before discarding older tracked failures. */ - public AuthenticationFailureTracker(int maxAttempts, int banDuration, + public InMemoryAuthenticationFailureTracker(int maxAttempts, int banDuration, long maxAddresses) { this.maxAttempts = maxAttempts; this.banDuration = banDuration; - // Inform administrator of configured behavior - if (maxAttempts <= 0) { - logger.info("Maximum failed authentication attempts has been set " - + "to {}. Automatic banning of brute-force authentication " - + "attempts will be disabled.", maxAttempts); - } - else if (banDuration <= 0) { - logger.info("Ban duration for addresses that repeatedly fail " - + "authentication has been set to {}. Automatic banning " - + "of brute-force authentication attempts will be " - + "disabled.", banDuration); - } - else { - logger.info("Addresses will be automatically banned for {} " - + "seconds after {} failed authentication attempts.", - banDuration, maxAttempts); - } - // Limit maximum number of tracked addresses to configured upper bound this.failures = Caffeine.newBuilder() .maximumSize(maxAddresses) .build(); - logger.info("Up to {} unique addresses will be tracked/banned at any " - + " given time.", maxAddresses); - } /** @@ -187,10 +167,6 @@ public class AuthenticationFailureTracker { private void notifyAuthenticationStatus(Credentials credentials, boolean failed) throws GuacamoleException { - // Do not track/ban if tracking or banning are disabled - if (maxAttempts <= 0 || banDuration <= 0) - return; - // Ignore requests that do not contain explicit parameters of any kind if (isEmpty(credentials)) return; @@ -234,54 +210,19 @@ public class AuthenticationFailureTracker { } - /** - * Reports that an authentication request has been received, but it is - * either not yet known whether the request has succeeded or failed. If the - * associated address is currently being blocked, an exception will be - * thrown. - * - * @param credentials - * The credentials associated with the authentication request. - * - * @throws GuacamoleException - * If the authentication request is being blocked due to brute force - * prevention rules. - */ + @Override public void notifyAuthenticationRequestReceived(Credentials credentials) throws GuacamoleException { notifyAuthenticationStatus(credentials, false); } - /** - * Reports that an authentication request has been received and has - * succeeded. If the associated address is currently being blocked, an - * exception will be thrown. - * - * @param credentials - * The credentials associated with the successful authentication - * request. - * - * @throws GuacamoleException - * If the authentication request is being blocked due to brute force - * prevention rules. - */ + @Override public void notifyAuthenticationSuccess(Credentials credentials) throws GuacamoleException { notifyAuthenticationStatus(credentials, false); } - /** - * Reports that an authentication request has been received and has - * failed. If the associated address is currently being blocked, an - * exception will be thrown. - * - * @param credentials - * The credentials associated with the failed authentication request. - * - * @throws GuacamoleException - * If the authentication request is being blocked due to brute force - * prevention rules. - */ + @Override public void notifyAuthenticationFailed(Credentials credentials) throws GuacamoleException { notifyAuthenticationStatus(credentials, true); diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/NullAuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/NullAuthenticationFailureTracker.java new file mode 100644 index 000000000..9b50a3022 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/NullAuthenticationFailureTracker.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.ban.status; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.Credentials; + +/** + * AuthenticationFailureTracker implementation that does nothing. All requests + * are ignored, regardless of status, and no tracking is performed. + */ +public class NullAuthenticationFailureTracker implements AuthenticationFailureTracker { + + @Override + public void notifyAuthenticationRequestReceived(Credentials credentials) + throws GuacamoleException { + // Do nothing + } + + @Override + public void notifyAuthenticationSuccess(Credentials credentials) + throws GuacamoleException { + // Do nothing + } + + @Override + public void notifyAuthenticationFailed(Credentials credentials) + throws GuacamoleException { + // Do nothing + } + +} From 2e5d3f4fafcfe475cd8c7bccaba4d5eae94ca560 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 22 Aug 2022 15:12:01 -0700 Subject: [PATCH 063/289] GUACAMOLE-990: Disable tracking if max addresses is not a positive integer. --- .../auth/ban/BanningAuthenticationProvider.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java index a6df43a30..1d115d39d 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java @@ -22,7 +22,6 @@ package org.apache.guacamole.auth.ban; import org.apache.guacamole.auth.ban.status.InMemoryAuthenticationFailureTracker; import org.apache.guacamole.auth.ban.status.AuthenticationFailureTracker; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.ban.status.NullAuthenticationFailureTracker; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.LocalEnvironment; @@ -130,12 +129,6 @@ public class BanningAuthenticationProvider extends AbstractAuthenticationProvide int banDuration = environment.getProperty(IP_BAN_DURATION, DEFAULT_IP_BAN_DURATION); long maxAddresses = environment.getProperty(MAX_ADDRESSES, DEFAULT_MAX_ADDRESSES); - if (maxAddresses <= 0) - throw new GuacamoleServerException("The maximum number of " - + "addresses tracked, as specified by the " - + "\"" + MAX_ADDRESSES.getName() + "\" property, must be " - + "greater than zero."); - // Configure auth failure tracking behavior and inform administrator of // ultimate result if (maxAttempts <= 0) { @@ -151,6 +144,12 @@ public class BanningAuthenticationProvider extends AbstractAuthenticationProvide + "of brute-force authentication attempts will be " + "disabled.", banDuration); } + else if (maxAddresses <= 0) { + this.tracker = new NullAuthenticationFailureTracker(); + logger.info("Maximum number of tracked addresses has been set to " + + "{}. Automatic banning of brute-force authentication " + + "attempts will be disabled.", maxAddresses); + } else { this.tracker = new InMemoryAuthenticationFailureTracker(maxAttempts, banDuration, maxAddresses); logger.info("Addresses will be automatically banned for {} " From e0a9364dde890bff48481ad9e95affa38a77abb4 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 17 Aug 2022 18:05:41 +0000 Subject: [PATCH 064/289] GUACAMOLE-1661: Simplify and clarify KSM domain search code. --- .../guacamole/vault/ksm/secret/KsmClient.java | 16 +++--- .../vault/ksm/secret/KsmSecretService.java | 4 +- .../{UserDomain.java => UserLogin.java} | 50 +++++++------------ 3 files changed, 30 insertions(+), 40 deletions(-) rename extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/{UserDomain.java => UserLogin.java} (63%) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index 34f350234..a573259b5 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -177,7 +177,7 @@ public class KsmClient { * this Map, {@link #cachedAmbiguousUsers} must first be checked to * verify that there is indeed only one record associated with that user. */ - private final Map cachedRecordsByUser = new HashMap<>(); + private final Map cachedRecordsByUser = new HashMap<>(); /** * The set of all username/domain combos that are associated with multiple @@ -187,7 +187,7 @@ public class KsmClient { * acquired appropriately. This Set must be checked before using a value * retrieved from {@link #cachedRecordsByUser}. */ - private final Set cachedAmbiguousUsers = new HashSet<>(); + private final Set cachedAmbiguousUsers = new HashSet<>(); /** * All records retrieved from Keeper Secrets Manager, where each key is the @@ -307,10 +307,12 @@ public class KsmClient { WindowsUsername usernameAndDomain = ( WindowsUsername.splitWindowsUsernameFromDomain(username)); - // Use the username-split domain if not already set explicitly - if (usernameAndDomain.hasDomain()) + // Use the username-split domain if available + if (usernameAndDomain.hasDomain()) { domain = usernameAndDomain.getDomain(); + username = usernameAndDomain.getUsername(); addRecordForDomain(record, domain); + } } @@ -407,7 +409,7 @@ public class KsmClient { if (username == null) return; - UserDomain userDomain = new UserDomain(username, domain); + UserLogin userDomain = new UserLogin(username, domain); KeeperRecord existing = cachedRecordsByUser.putIfAbsent( userDomain, record); if (existing != null && record != existing) @@ -504,7 +506,7 @@ public class KsmClient { * The username of the record to return. * * @param domain - * The domain of the record to return. + * The domain of the record to return, or null if no domain exists. * * @return * The record associated with the given username and domain, or null @@ -519,7 +521,7 @@ public class KsmClient { validateCache(); cacheLock.readLock().lock(); - UserDomain userDomain = new UserDomain(username, domain); + UserLogin userDomain = new UserLogin(username, domain); try { diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 3b8a6dc0d..cb5868cfa 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -373,7 +373,9 @@ public class KsmSecretService implements VaultSecretService { filter.filter(gatewayUsername), filteredGatewayDomain)); - } else { + } + + else { // Retrieve and define user-specific tokens, if any // NOTE that non-RDP connections do not have a domain diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserDomain.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserLogin.java similarity index 63% rename from extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserDomain.java rename to extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserLogin.java index bb2c2309d..5cd50ee6c 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserDomain.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/UserLogin.java @@ -19,6 +19,8 @@ package org.apache.guacamole.vault.ksm.secret; +import java.util.Objects; + import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -29,7 +31,7 @@ import javax.annotation.Nullable; * actually be identified by both the user and domain, if the appropriate * settings are enabled. */ -class UserDomain { +class UserLogin { /** * The username associated with the user record. @@ -44,17 +46,17 @@ class UserDomain { private final String domain; /** - * Create a new UserDomain instance with the provided username and + * Create a new UserLogin instance with the provided username and * domain. The domain may be null, but the username should never be. * * @param username - * The username to create the UserDomain instance with. This should + * The username to create the UserLogin instance with. This should * never be null. * * @param domain - * The domain to create the UserDomain instance with. This can be null. + * The domain to create the UserLogin instance with. This can be null. */ - UserDomain(@Nonnull String username, @Nullable String domain) { + UserLogin(@Nonnull String username, @Nullable String domain) { this.username = username; this.domain = domain; } @@ -62,13 +64,7 @@ class UserDomain { @Override public int hashCode() { - final int prime = 31; - - int result = 1; - result = prime * result + ((domain == null) ? 0 : domain.hashCode()); - result = prime * result + ((username == null) ? 0 : username.hashCode()); - - return result; + return Objects.hash(domain, username); } @@ -83,33 +79,23 @@ class UserDomain { if (obj == null) return false; - // Check if the other object is also a UserDomain + // Check if the other object is also a UserLogin if (getClass() != obj.getClass()) return false; - // If it is a UserDomain, it must have the same username... - UserDomain other = (UserDomain) obj; - if (username == null) { - if (other.username != null) - return false; - } else if (!username.equals(other.username)) - return false; + // If the other object is also a UserLogin, it must + // have the same username and domain + UserLogin other = (UserLogin) obj; + return Objects.equals(username, other.username) + && Objects.equals(domain, other.domain); - // .. and the same domain - if (domain == null) { - if (other.domain != null) - return false; - } else if (!domain.equals(other.domain)) - return false; - - return true; } /** - * Get the username associated with this UserDomain. + * Get the username associated with this UserLogin. * * @return - * The username associated with this UserDomain. + * The username associated with this UserLogin. */ public String getUsername() { return username; @@ -117,10 +103,10 @@ class UserDomain { /** - * Get the domain associated with this UserDomain. + * Get the domain associated with this UserLogin. * * @return - * The domain associated with this UserDomain. + * The domain associated with this UserLogin. */ public String getDomain() { return domain; From 2b997a9992cb246ae9e6aecbb52e9daed2680d91 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 24 Aug 2022 19:03:16 +0000 Subject: [PATCH 065/289] GUACAMOLE-1661: Restore logic to not index records by login if hostname is already defined. --- .../org/apache/guacamole/vault/ksm/secret/KsmClient.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index a573259b5..fd339dac5 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -322,8 +322,11 @@ public class KsmClient { if (!confService.getMatchUserRecordsByDomain()) domain = null; - // Store the login by username and domain - addRecordForLogin(record, username, domain); + // Store based on login ONLY if no hostname (will otherwise + // result in ambiguous entries for servers tied to identical + // accounts) + if (hostname == null) + addRecordForLogin(record, username, domain); } From c7bb1cb50c6dae7efd8f85f39ef5320d787a6696 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 25 Aug 2022 00:03:18 +0000 Subject: [PATCH 066/289] GUACAMOLE-1661: Parse config only once when iterating records. --- .../guacamole/vault/ksm/secret/KsmClient.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index fd339dac5..6e0bdf177 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -277,13 +277,13 @@ public class KsmClient { cachedAmbiguousDomains.clear(); cachedRecordsByDomain.clear(); + // Parse configuration + final boolean shouldSplitUsernames = confService.getSplitWindowsUsernames(); + final boolean shouldMatchByDomain = confService.getMatchUserRecordsByDomain(); + // Store all records, sorting each into host-based, login-based, // and domain-based buckets - Iterator recordIterator = records.iterator(); - while(recordIterator.hasNext()) { - - // Go through records one at a time - KeeperRecord record = recordIterator.next(); + records.forEach(record -> { // Store based on UID ... cachedRecordsByUid.put(record.getRecordUid(), record); @@ -300,8 +300,7 @@ public class KsmClient { String username = recordService.getUsername(record); // If we have a username, and there isn't already a domain explicitly defined - if (username != null && domain == null - && confService.getSplitWindowsUsernames()) { + if (username != null && domain == null && shouldSplitUsernames) { // Attempt to split out the domain of the username WindowsUsername usernameAndDomain = ( @@ -319,7 +318,7 @@ public class KsmClient { // If domain matching is not enabled for user records, // explicitly set all domains to null to allow matching // on username only - if (!confService.getMatchUserRecordsByDomain()) + if (!shouldMatchByDomain) domain = null; // Store based on login ONLY if no hostname (will otherwise @@ -328,7 +327,7 @@ public class KsmClient { if (hostname == null) addRecordForLogin(record, username, domain); - } + }); // Cache has been refreshed this.cacheTimestamp = System.currentTimeMillis(); From e4c65cba19fadc87ff61500d2065766472398ad2 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 19 Jul 2022 18:31:40 +0000 Subject: [PATCH 067/289] GUACAMOLE-1656: Add per-user KSM vault functionality. --- .../vault/conf/VaultAttributeService.java | 30 +++ .../vault/user/VaultUserContext.java | 59 ++++-- .../ksm/KsmAuthenticationProviderModule.java | 1 + .../vault/ksm/conf/KsmAttributeService.java | 66 ++++++- .../ksm/conf/KsmConfigurationService.java | 26 +++ .../vault/ksm/secret/KsmSecretService.java | 177 +++++++++++------- .../vault/ksm/user/KsmConnection.java | 82 ++++++++ .../vault/ksm/user/KsmDirectoryService.java | 87 +++++++++ .../guacamole/vault/ksm/user/KsmUser.java | 82 ++++++++ .../src/main/resources/translations/en.json | 10 + .../net/auth/DelegatingUserContext.java | 5 + .../guacamole/net/auth/UserContext.java | 17 ++ .../controllers/manageUserController.js | 2 + .../src/app/rest/services/schemaService.js | 28 +++ .../directives/guacSettingsPreferences.js | 124 +++++++++++- .../templates/settingsPreferences.html | 10 + .../main/frontend/src/translations/en.json | 3 + guacamole/src/main/frontend/webpack.config.js | 12 -- .../guacamole/rest/schema/SchemaResource.java | 20 ++ .../guacamole/rest/user/UserResource.java | 53 +++++- 20 files changed, 785 insertions(+), 109 deletions(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java index 2bd08cde6..bccf08e03 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/conf/VaultAttributeService.java @@ -30,6 +30,16 @@ import org.apache.guacamole.form.Form; */ public interface VaultAttributeService { + /** + * Return all custom connection attributes to be exposed through the + * admin UI for the current vault implementation. + * + * @return + * All custom connection attributes to be exposed through the + * admin UI for the current vault implementation. + */ + public Collection getConnectionAttributes(); + /** * Return all custom connection group attributes to be exposed through the * admin UI for the current vault implementation. @@ -39,4 +49,24 @@ public interface VaultAttributeService { * admin UI for the current vault implementation. */ public Collection getConnectionGroupAttributes(); + + /** + * Return all custom user attributes to be exposed through the admin UI for + * the current vault implementation. + * + * @return + * All custom user attributes to be exposed through the admin UI for + * the current vault implementation. + */ + public Collection getUserAttributes(); + + /** + * Return all user preference attributes to be exposed through the user + * preferences UI for the current vault implementation. + * + * @return + * All user preference attributes to be exposed through the user + * preferences UI for the current vault implementation. + */ + public Collection getUserPreferenceAttributes(); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index 8e8f66853..748237dd2 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java @@ -241,7 +241,7 @@ public class VaultUserContext extends TokenInjectingUserContext { * * @throws GuacamoleException * If the value for any applicable secret cannot be retrieved from the - * vault due to an error. + * vault due to an error.1 */ private Map> getTokens( Connectable connectable, Map tokenMapping, @@ -407,7 +407,6 @@ public class VaultUserContext extends TokenInjectingUserContext { TokenFilter filter = createFilter(); filter.setToken(CONNECTION_NAME_TOKEN, connection.getName()); filter.setToken(CONNECTION_IDENTIFIER_TOKEN, identifier); - // Add hostname and username tokens if available (implementations are // not required to expose connection configuration details) @@ -439,17 +438,6 @@ public class VaultUserContext extends TokenInjectingUserContext { } - @Override - public Collection getConnectionGroupAttributes() { - - // Add any custom attributes to any previously defined attributes - return Collections.unmodifiableCollection(Stream.concat( - super.getConnectionGroupAttributes().stream(), - attributeService.getConnectionGroupAttributes().stream() - ).collect(Collectors.toList())); - - } - @Override public Directory getUserDirectory() throws GuacamoleException { @@ -490,6 +478,51 @@ public class VaultUserContext extends TokenInjectingUserContext { // Defer to the vault-specific directory service return directoryService.getSharingProfileDirectory(super.getSharingProfileDirectory()); + + } + + @Override + public Collection getUserAttributes() { + + // Add any custom attributes to any previously defined attributes + return Collections.unmodifiableCollection(Stream.concat( + super.getUserAttributes().stream(), + attributeService.getUserAttributes().stream() + ).collect(Collectors.toList())); + + } + + @Override + public Collection getUserPreferenceAttributes() { + + // Add any custom preference attributes to any previously defined attributes + return Collections.unmodifiableCollection(Stream.concat( + super.getUserPreferenceAttributes().stream(), + attributeService.getUserPreferenceAttributes().stream() + ).collect(Collectors.toList())); + + } + + @Override + public Collection getConnectionAttributes() { + + // Add any custom attributes to any previously defined attributes + return Collections.unmodifiableCollection(Stream.concat( + super.getConnectionAttributes().stream(), + attributeService.getConnectionAttributes().stream() + ).collect(Collectors.toList())); + + } + + @Override + public Collection getConnectionGroupAttributes() { + + // Add any custom attributes to any previously defined attributes + return Collections.unmodifiableCollection(Stream.concat( + super.getConnectionGroupAttributes().stream(), + attributeService.getConnectionGroupAttributes().stream() + ).collect(Collectors.toList())); + } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java index 3c28553e0..faf22aa4e 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java @@ -57,6 +57,7 @@ public class KsmAuthenticationProviderModule // Bind services specific to Keeper Secrets Manager bind(KsmRecordService.class); + bind(KsmAttributeService.class); bind(VaultAttributeService.class).to(KsmAttributeService.class); bind(VaultConfigurationService.class).to(KsmConfigurationService.class); bind(VaultSecretService.class).to(KsmSecretService.class); diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java index 83ab9c4a3..bdceb4c21 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java @@ -23,10 +23,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.form.BooleanField; import org.apache.guacamole.form.Form; import org.apache.guacamole.form.TextField; import org.apache.guacamole.vault.conf.VaultAttributeService; +import com.google.inject.Inject; import com.google.inject.Singleton; /** @@ -36,28 +39,81 @@ import com.google.inject.Singleton; @Singleton public class KsmAttributeService implements VaultAttributeService { + + @Inject + private KsmConfigurationService configurationService; + /** * The name of the attribute which can contain a KSM configuration blob - * associated with a connection group. + * associated with either a connection group or user. */ public static final String KSM_CONFIGURATION_ATTRIBUTE = "ksm-config"; /** * All attributes related to configuring the KSM vault on a - * per-connection-group basis. + * per-connection-group or per-user basis. */ public static final Form KSM_CONFIGURATION_FORM = new Form("ksm-config", Arrays.asList(new TextField(KSM_CONFIGURATION_ATTRIBUTE))); /** - * All KSM-specific connection group attributes, organized by form. + * All KSM-specific attributes for users or connection groups, organized by form. */ - public static final Collection KSM_CONNECTION_GROUP_ATTRIBUTES = + public static final Collection KSM_ATTRIBUTES = Collections.unmodifiableCollection(Arrays.asList(KSM_CONFIGURATION_FORM)); + /** + * The name of the attribute which can controls whether a KSM user configuration + * is enabled on a connection-by-connection basis. + */ + public static final String KSM_USER_CONFIG_ENABLED_ATTRIBUTE = "ksm-user-config-enabled"; + + /** + * The string value used by KSM attributes to represent the boolean value "true". + */ + public static final String TRUTH_VALUE = "true"; + + /** + * All attributes related to configuring the KSM vault on a per-connection basis. + */ + public static final Form KSM_CONNECTION_FORM = new Form("ksm-config", + Arrays.asList(new BooleanField(KSM_USER_CONFIG_ENABLED_ATTRIBUTE, TRUTH_VALUE))); + + /** + * All KSM-specific attributes for connections, organized by form. + */ + public static final Collection KSM_CONNECTION_ATTRIBUTES = + Collections.unmodifiableCollection(Arrays.asList(KSM_CONNECTION_FORM)); + + @Override + public Collection getConnectionAttributes() { + return KSM_CONNECTION_ATTRIBUTES; + } + @Override public Collection getConnectionGroupAttributes() { - return KSM_CONNECTION_GROUP_ATTRIBUTES; + return KSM_ATTRIBUTES; } + @Override + public Collection getUserAttributes() { + return KSM_ATTRIBUTES; + } + + @Override + public Collection getUserPreferenceAttributes() { + + try { + + // Expose the user attributes IFF user-level KSM configuration is enabled + return configurationService.getAllowUserConfig() ? KSM_ATTRIBUTES : Collections.emptyList(); + + } catch (GuacamoleException e) { + + // If the configuration can't be parsed, default to not exposing the attribute + return Collections.emptyList(); + } + } + + } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java index c9a9536ea..9d2e4565f 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java @@ -84,6 +84,17 @@ public class KsmConfigurationService extends VaultConfigurationService { } }; + /** + * Whether users should be able to supply their own KSM configurations. + */ + private static final BooleanGuacamoleProperty ALLOW_USER_CONFIG = new BooleanGuacamoleProperty() { + + @Override + public String getName() { + return "ksm-allow-user-config"; + } + }; + /** * Whether windows domains should be stripped off from usernames that are * read from the KSM vault. @@ -138,6 +149,21 @@ public class KsmConfigurationService extends VaultConfigurationService { return environment.getProperty(ALLOW_UNVERIFIED_CERT, false); } + /** + * Return whether users should be able to provide their own KSM configs. + * + * @return + * true if users should be able to provide their own KSM configs, + * false otherwise. + * + * @throws GuacamoleException + * If the value specified within guacamole.properties cannot be + * parsed. + */ + public boolean getAllowUserConfig() throws GuacamoleException { + return environment.getProperty(ALLOW_USER_CONFIG, false); + } + @Override public boolean getSplitWindowsUsernames() throws GuacamoleException { return environment.getProperty(STRIP_WINDOWS_DOMAINS, false); diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index cb5868cfa..a3b772256 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -26,8 +26,11 @@ import com.keepersecurity.secretsManager.core.SecretsManagerOptions; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -304,6 +307,34 @@ public class KsmSecretService implements VaultSecretService { } + /** + * Returns true if user-level KSM configuration is enabled for the given + * Connectable, false otherwise. + * + * @param connectable + * The connectable to check for whether user-level KSM configs are + * enabled. + * + * @return + * True if user-level KSM configuration is enabled for the given + * Connectable, false otherwise. + */ + private boolean isKsmUserConfigEnabled(Connectable connectable) { + + // If it's a connection, user-level config is enabled IFF the appropriate + // attribute is set to true + if (connectable instanceof Connection) + return KsmAttributeService.TRUTH_VALUE.equals(((Connection) connectable).getAttributes().get( + KsmAttributeService.KSM_USER_CONFIG_ENABLED_ATTRIBUTE)); + + // KSM token replacement is not enabled for balancing groups, so for + // now, user-level KSM configs will be explicitly disabled. + // TODO: If token replacement is implemented for balancing groups, + // implement this functionality for them as well. + return false; + + } + @Override public Map> getTokens(UserContext userContext, Connectable connectable, GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException { @@ -314,78 +345,92 @@ public class KsmSecretService implements VaultSecretService { // Attempt to find a KSM config for this connection or group String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable); - // Get a client instance for this KSM config - KsmClient ksm = getClient(ksmConfig); + // Create a list containing just the global / connection group config + List ksmClients = new ArrayList<>(2); + ksmClients.add(getClient(ksmConfig)); - // Retrieve and define server-specific tokens, if any - String hostname = parameters.get("hostname"); - if (hostname != null && !hostname.isEmpty()) - addRecordTokens(tokens, "KEEPER_SERVER_", - ksm.getRecordByHost(filter.filter(hostname))); + // Only use the user-specific KSM config if explicitly enabled in the global + // configuration, AND for the specific connectable being connected to + if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable)) { - // Tokens specific to RDP - if ("rdp".equals(config.getProtocol())) { - - // Retrieve and define gateway server-specific tokens, if any - String gatewayHostname = parameters.get("gateway-hostname"); - if (gatewayHostname != null && !gatewayHostname.isEmpty()) - addRecordTokens(tokens, "KEEPER_GATEWAY_", - ksm.getRecordByHost(filter.filter(gatewayHostname))); - - // Retrieve and define domain tokens, if any - String domain = parameters.get("domain"); - String filteredDomain = null; - if (domain != null && !domain.isEmpty()) { - filteredDomain = filter.filter(domain); - addRecordTokens(tokens, "KEEPER_DOMAIN_", - ksm.getRecordByDomain(filteredDomain)); - } - - // Retrieve and define gateway domain tokens, if any - String gatewayDomain = parameters.get("gateway-domain"); - String filteredGatewayDomain = null; - if (gatewayDomain != null && !gatewayDomain.isEmpty()) { - filteredGatewayDomain = filter.filter(gatewayDomain); - addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_", - ksm.getRecordByDomain(filteredGatewayDomain)); - } - - // If domain matching is disabled for user records, - // explicitly set the domains to null when storing - // user records to enable username-only matching - if (!confService.getMatchUserRecordsByDomain()) { - filteredDomain = null; - filteredGatewayDomain = null; - } - - // Retrieve and define user-specific tokens, if any - String username = parameters.get("username"); - if (username != null && !username.isEmpty()) - addRecordTokens(tokens, "KEEPER_USER_", - ksm.getRecordByLogin(filter.filter(username), - filteredDomain)); - - // Retrieve and define gateway user-specific tokens, if any - String gatewayUsername = parameters.get("gateway-username"); - if (gatewayUsername != null && !gatewayUsername.isEmpty()) - addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", - ksm.getRecordByLogin( - filter.filter(gatewayUsername), - filteredGatewayDomain)); + // Find a user-specific KSM config, if one exists + String userKsmConfig = userContext.self().getAttributes().get( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + // If a user-specific config exsts, process it first + if (userKsmConfig != null && !userKsmConfig.trim().isEmpty()) + ksmClients.add(0, getClient(userKsmConfig)); } - else { + // Iterate through the KSM clients, processing using the user-specific + // config first (if it exists), to ensure that any admin-defined values + // will override the user-speicifc values + Iterator ksmIterator = ksmClients.iterator(); + while (ksmIterator.hasNext()) { - // Retrieve and define user-specific tokens, if any - // NOTE that non-RDP connections do not have a domain - // field in the connection parameters, so the domain - // will always be null - String username = parameters.get("username"); - if (username != null && !username.isEmpty()) - addRecordTokens(tokens, "KEEPER_USER_", - ksm.getRecordByLogin(filter.filter(username), null)); - } + KsmClient ksm = ksmIterator.next(); + + // Retrieve and define server-specific tokens, if any + String hostname = parameters.get("hostname"); + if (hostname != null && !hostname.isEmpty()) + addRecordTokens(tokens, "KEEPER_SERVER_", + ksm.getRecordByHost(filter.filter(hostname))); + + // Tokens specific to RDP + if ("rdp".equals(config.getProtocol())) { + // Retrieve and define domain tokens, if any + String domain = parameters.get("domain"); + String filteredDomain = null; + if (domain != null && !domain.isEmpty()) { + filteredDomain = filter.filter(domain); + addRecordTokens(tokens, "KEEPER_DOMAIN_", + ksm.getRecordByDomain(filteredDomain)); + } + + // Retrieve and define gateway domain tokens, if any + String gatewayDomain = parameters.get("gateway-domain"); + String filteredGatewayDomain = null; + if (gatewayDomain != null && !gatewayDomain.isEmpty()) { + filteredGatewayDomain = filter.filter(gatewayDomain); + addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_", + ksm.getRecordByDomain(filteredGatewayDomain)); + } + + // If domain matching is disabled for user records, + // explicitly set the domains to null when storing + // user records to enable username-only matching + if (!confService.getMatchUserRecordsByDomain()) { + filteredDomain = null; + filteredGatewayDomain = null; + } + + // Retrieve and define user-specific tokens, if any + String username = parameters.get("username"); + if (username != null && !username.isEmpty()) + addRecordTokens(tokens, "KEEPER_USER_", + ksm.getRecordByLogin(filter.filter(username), + filteredDomain)); + + // Retrieve and define gateway user-specific tokens, if any + String gatewayUsername = parameters.get("gateway-username"); + if (gatewayUsername != null && !gatewayUsername.isEmpty()) + addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", + ksm.getRecordByLogin( + filter.filter(gatewayUsername), + filteredGatewayDomain)); + } + + else { + + // Retrieve and define user-specific tokens, if any + // NOTE that non-RDP connections do not have a domain + // field in the connection parameters, so the domain + // will always be null + String username = parameters.get("username"); + if (username != null && !username.isEmpty()) + addRecordTokens(tokens, "KEEPER_USER_", + ksm.getRecordByLogin(filter.filter(username), null)); + } return tokens; diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java new file mode 100644 index 000000000..da2866202 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java @@ -0,0 +1,82 @@ +/* + * 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.vault.ksm.user; + +import java.util.List; +import java.util.Map; + +import org.apache.guacamole.net.auth.DelegatingConnection; +import org.apache.guacamole.net.auth.Connection; + +import com.google.common.collect.Maps; + +/** + * A Connection that explicitly adds a blank entry for any defined + * KSM connection attributes. + */ +public class KsmConnection extends DelegatingConnection { + + /** + * The names of all connection attributes defined for the vault. + */ + private List connectionAttributeNames; + + /** + * Create a new Vault connection wrapping the provided Connection record. Any + * attributes defined in the provided connection attribute forms will have empty + * values automatically populated when getAttributes() is called. + * + * @param connection + * The connection record to wrap. + * + * @param connectionAttributeNames + * The names of all connection attributes to automatically expose. + */ + KsmConnection(Connection connection, List connectionAttributeNames) { + + super(connection); + this.connectionAttributeNames = connectionAttributeNames; + + } + + /** + * Return the underlying wrapped connection record. + * + * @return + * The wrapped connection record. + */ + Connection getUnderlyingConnection() { + return getDelegateConnection(); + } + + @Override + public Map getAttributes() { + + // Make a copy of the existing map + Map attributeMap = Maps.newHashMap(super.getAttributes()); + + // Add every defined attribute + connectionAttributeNames.forEach( + attributeName -> attributeMap.putIfAbsent(attributeName, null)); + + return attributeMap; + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java index b924bc524..3eff928fe 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java @@ -26,13 +26,16 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.net.auth.Attributes; +import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.DecoratingDirectory; import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfig; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; @@ -57,6 +60,12 @@ public class KsmDirectoryService extends VaultDirectoryService { @Inject private KsmConfigurationService configurationService; + /** + * Service for retrieving KSM-specific attributes. + */ + @Inject + private KsmAttributeService ksmAttributeService; + /** * A singleton ObjectMapper for converting a Map to a JSON string when * generating a base64-encoded JSON KSM config blob. @@ -222,6 +231,36 @@ public class KsmDirectoryService extends VaultDirectoryService { } + @Override + public Directory getConnectionDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // A Connection directory that will intercept add and update calls to + // validate KSM configurations, and translate one-time-tokens, if possible + return new DecoratingDirectory(underlyingDirectory) { + + @Override + protected Connection decorate(Connection connection) throws GuacamoleException { + + // Wrap in a KsmConnection class to ensure that all defined KSM fields will be + // present + return new KsmConnection( + connection, + ksmAttributeService.getConnectionAttributes().stream().flatMap( + form -> form.getFields().stream().map(field -> field.getName()) + ).collect(Collectors.toList())); + } + + @Override + protected Connection undecorate(Connection connection) throws GuacamoleException { + + // Unwrap the KsmUser + return ((KsmConnection) connection).getUnderlyingConnection(); + } + + }; + } + @Override public Directory getConnectionGroupDirectory( Directory underlyingDirectory) throws GuacamoleException { @@ -269,4 +308,52 @@ public class KsmDirectoryService extends VaultDirectoryService { }; } + + @Override + public Directory getUserDirectory( + Directory underlyingDirectory) throws GuacamoleException { + + // A User directory that will intercept add and update calls to + // validate KSM configurations, and translate one-time-tokens, if possible + return new DecoratingDirectory(underlyingDirectory) { + + @Override + public void add(User user) throws GuacamoleException { + + // Check for the KSM config attribute and translate the one-time token + // if possible before adding + processAttributes(user); + super.add(user); + } + + @Override + public void update(User user) throws GuacamoleException { + + // Check for the KSM config attribute and translate the one-time token + // if possible before updating + processAttributes(user); + super.update(user); + } + + @Override + protected User decorate(User user) throws GuacamoleException { + + // Wrap in a KsmUser class to ensure that all defined KSM fields will be + // present + return new KsmUser( + user, + ksmAttributeService.getUserAttributes().stream().flatMap( + form -> form.getFields().stream().map(field -> field.getName()) + ).collect(Collectors.toList())); + } + + @Override + protected User undecorate(User user) throws GuacamoleException { + + // Unwrap the KsmUser + return ((KsmUser) user).getUnderlyingUser(); + } + + }; + } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java new file mode 100644 index 000000000..a846eb0d8 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java @@ -0,0 +1,82 @@ +/* + * 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.vault.ksm.user; + +import java.util.List; +import java.util.Map; + +import org.apache.guacamole.net.auth.DelegatingUser; +import org.apache.guacamole.net.auth.User; + +import com.google.common.collect.Maps; + +/** + * A User that explicitly adds a blank entry for any defined + * KSM user attributes. + */ +public class KsmUser extends DelegatingUser { + + /** + * The names of all user attributes defined for the vault. + */ + private List userAttributeNames; + + /** + * Create a new Vault user wrapping the provided User record. Any + * attributes defined in the provided user attribute forms will have empty + * values automatically populated when getAttributes() is called. + * + * @param user + * The user record to wrap. + * + * @param userAttributeNames + * The names of all user attributes to automatically expose. + */ + KsmUser(User user, List userAttributeNames) { + + super(user); + this.userAttributeNames = userAttributeNames; + + } + + /** + * Return the underlying wrapped user record. + * + * @return + * The wrapped user record. + */ + User getUnderlyingUser() { + return getDelegateUser(); + } + + @Override + public Map getAttributes() { + + // Make a copy of the existing map + Map attributeMap = Maps.newHashMap(super.getAttributes()); + + // Add every defined attribute + userAttributeNames.forEach( + attributeName -> attributeMap.putIfAbsent(attributeName, null)); + + return attributeMap; + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json index 4601a13f1..034f5f24a 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/resources/translations/en.json @@ -4,12 +4,22 @@ "NAME" : "Keeper Secrets Manager" }, + "CONNECTION_ATTRIBUTES" : { + "SECTION_HEADER_KSM_CONFIG" : "Keeper Secrets Manager", + "FIELD_HEADER_KSM_USER_CONFIG_ENABLED" : "Allow user-provided KSM configuration" + }, + "CONNECTION_GROUP_ATTRIBUTES" : { "SECTION_HEADER_KSM_CONFIG" : "Keeper Secrets Manager", "FIELD_HEADER_KSM_CONFIG" : "KSM Service Configuration ", "ERROR_INVALID_KSM_CONFIG_BLOB" : "The provided base64-encoded KSM configuration blob is not valid. Please ensure that you have copied the entire blob.", "ERROR_INVALID_KSM_ONE_TIME_TOKEN" : "The provided configuration is not a valid KSM one-time token or base64-encoded configuration blob. Please ensure that you have copied the entire token value." + }, + + "USER_ATTRIBUTES" : { + "SECTION_HEADER_KSM_CONFIG" : "Keeper Secrets Manager", + "FIELD_HEADER_KSM_CONFIG" : "KSM Service Configuration " } } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java index 85e025909..4b0343181 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java @@ -127,6 +127,11 @@ public class DelegatingUserContext implements UserContext { return userContext.getUserAttributes(); } + @Override + public Collection getUserPreferenceAttributes() { + return userContext.getUserPreferenceAttributes(); + } + @Override public Collection getUserGroupAttributes() { return userContext.getUserGroupAttributes(); diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java index ccdcaae09..3f75b899c 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java @@ -20,6 +20,8 @@ package org.apache.guacamole.net.auth; import java.util.Collection; +import java.util.Collections; + import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.form.Form; @@ -211,6 +213,21 @@ public interface UserContext { */ Collection getUserAttributes(); + /** + * Retrieves a collection of user attributes, specific to the user preferences + * page in the UI. Unlike standard user attributes, these should be self-editable. + * + * @return + * A collection of form of user attributes, specific to the user preferences + * page in the UI. + */ + default Collection getUserPreferenceAttributes() { + + // By default, a user context does not expose any preference user attributes + return Collections.emptyList(); + + } + /** * Retrieves a collection of all attributes applicable to user groups. This * collection will contain only those attributes which the current user has diff --git a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js b/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js index f7ead136a..94e3d8944 100644 --- a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js @@ -501,4 +501,6 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return userService.deleteUser($scope.dataSource, $scope.user); }; + console.log($scope); + }]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/schemaService.js b/guacamole/src/main/frontend/src/app/rest/services/schemaService.js index dee10e1bd..d6afa2b75 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/schemaService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/schemaService.js @@ -58,6 +58,34 @@ angular.module('rest').factory('schemaService', ['$injector', }; + /** + * Makes a request to the REST API to get the list of available user preference + * attributes, returning a promise that provides an array of @link{Form} objects + * if successful. Each element of the array describes a logical grouping of + * possible user preference attributes. + * + * @param {String} dataSource + * The unique identifier of the data source containing the users whose + * available user preference attributes are to be retrieved. This + * identifier corresponds to an AuthenticationProvider within the + * Guacamole web application. + * + * @returns {Promise.} + * A promise which will resolve with an array of @link{Form} + * objects, where each @link{Form} describes a logical grouping of + * possible attributes. + */ + service.getUserPreferenceAttributes = function getUserPreferenceAttributes(dataSource) { + + // Retrieve available user attributes + return authenticationService.request({ + cache : cacheService.schema, + method : 'GET', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userPreferenceAttributes' + }); + + }; + /** * Makes a request to the REST API to get the list of available attributes * for user group objects, returning a promise that provides an array of diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js index aad0a2e0e..5824eb194 100644 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js +++ b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js @@ -21,7 +21,7 @@ * A directive for managing preferences local to the current user. */ angular.module('settings').directive('guacSettingsPreferences', [function guacSettingsPreferences() { - + return { // Element only restrict: 'E', @@ -33,16 +33,18 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe controller: ['$scope', '$injector', function settingsPreferencesController($scope, $injector) { // Get required types - var PermissionSet = $injector.get('PermissionSet'); + const Form = $injector.get('Form'); + const PermissionSet = $injector.get('PermissionSet'); // Required services - var $translate = $injector.get('$translate'); - var authenticationService = $injector.get('authenticationService'); - var guacNotification = $injector.get('guacNotification'); - var permissionService = $injector.get('permissionService'); - var preferenceService = $injector.get('preferenceService'); - var requestService = $injector.get('requestService'); - var userService = $injector.get('userService'); + const $translate = $injector.get('$translate'); + const authenticationService = $injector.get('authenticationService'); + const guacNotification = $injector.get('guacNotification'); + const permissionService = $injector.get('permissionService'); + const preferenceService = $injector.get('preferenceService'); + const requestService = $injector.get('requestService'); + const schemaService = $injector.get('schemaService'); + const userService = $injector.get('userService'); /** * An action to be provided along with the object sent to @@ -56,6 +58,13 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe } }; + /** + * The user being modified. + * + * @type User + */ + $scope.user = null; + /** * The username of the current user. * @@ -78,6 +87,26 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe */ $scope.preferences = preferenceService.preferences; + /** + * All available user attributes, as a mapping of form name to form + * object. The form object contains a name, as well as a Map of fields. + * + * The Map type is used here to maintain form/name uniqueness, as well as + * insertion order, to ensure a consistent UI experience. + * + * @type Map + */ + $scope.attributeMap = new Map(); + + /** + * All available user attributes. This is only the set of attribute + * definitions, organized as logical groupings of attributes, not attribute + * values. + * + * @type Form[] + */ + $scope.attributes = null; + /** * The fields which should be displayed for choosing locale * preferences. Each field name must be a property on @@ -197,7 +226,82 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe }; + + /** + * Saves the current user, displaying an acknowledgement message if + * saving was successful, or an error if the save failed. + */ + $scope.saveUser = function saveUser() { + return userService.saveUser(dataSource, $scope.user) + .then(() => guacNotification.showStatus({ + text : { + key : 'SETTINGS_PREFERENCES.INFO_PREFERENCE_ATTRIBUTES_CHANGED' + }, + actions : [ ACKNOWLEDGE_ACTION ] + }), + guacNotification.SHOW_REQUEST_ERROR); + }; + + // Fetch the user record + userService.getUser(dataSource, username).then(function saveUserData(user) { + $scope.user = user; + }) + + // Get all datasources that are available for this user + authenticationService.getAvailableDataSources().forEach(function loadAttributesForDataSource(dataSource) { + + // Fetch all user attribute forms defined for the datasource + schemaService.getUserPreferenceAttributes(dataSource).then(function saveAttributes(attributes) { + + // Iterate through all attribute forms + attributes.forEach(function addAttribute(attributeForm) { + + // If the form with the retrieved name already exists + if ($scope.attributeMap.has(attributeForm.name)) { + const existingFields = $scope.attributeMap.get(attributeForm.name).fields; + + // Add each field to the existing list for this form + attributeForm.fields.forEach(function addAllFieldsToExistingMap(field) { + existingFields.set(field.name, field); + }) + } + + else { + + // Create a new entry for the form + $scope.attributeMap.set(attributeForm.name, { + name: attributeForm.name, + + // With the field array from the API converted into a Map + fields: attributeForm.fields.reduce( + function addFieldToMap(currentFieldMap, field) { + currentFieldMap.set(field.name, field); + return currentFieldMap; + }, new Map() + ) + + }) + } + + }); + + // Re-generate the attributes array every time + $scope.attributes = Array.of(...$scope.attributeMap.values()).map(function convertFieldsToArray(formObject) { + + // Convert each temporary form object to a Form type + return new Form({ + name: formObject.name, + + // Convert the field map to a simple array of fields + fields: Array.of(...formObject.fields.values()) + }) + }); + + }); + + }); + }] }; - + }]); diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html index cabd2ae9f..ac5cc4f59 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html @@ -89,4 +89,14 @@

+ +

{{'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_ATTRIBUTES' | translate}}

+
+ + + + +
+ diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 05a9fa279..7f52fea11 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -916,6 +916,7 @@ "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", @@ -942,6 +943,7 @@ "HELP_UPDATE_PASSWORD" : "If you wish to change your password, enter your current password and the desired new password below, and click \"Update Password\". The change will take effect immediately.", "INFO_PASSWORD_CHANGED" : "Password changed.", + "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "User attributes saved.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", @@ -949,6 +951,7 @@ "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Default Input Method", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Default Mouse Emulation Mode", + "SECTION_HEADER_UPDATE_ATTRIBUTES" : "User Attributes", "SECTION_HEADER_UPDATE_PASSWORD" : "Change Password" }, diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index 29bb8ddd5..3f1574fa1 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -77,18 +77,6 @@ module.exports = { ] }, optimization: { - minimizer: [ - - // Minify using Google Closure Compiler - new ClosureWebpackPlugin({ mode: 'STANDARD' }, { - languageIn: 'ECMASCRIPT_2020', - languageOut: 'ECMASCRIPT5', - compilationLevel: 'SIMPLE' - }), - - new CssMinimizerPlugin() - - ], splitChunks: { cacheGroups: { diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java index 9086ac93e..edc125127 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java @@ -77,6 +77,26 @@ public class SchemaResource { } + /** + * Retrieves the possible user preference attributes of a user object. + * + * @return + * A collection of forms which describe the possible preference attributes of a + * user object. + * + * @throws GuacamoleException + * If an error occurs while retrieving the possible attributes. + */ + @GET + @Path("userPreferenceAttributes") + public Collection getUserAttrigetUserPreferenceAttributesbutes() + throws GuacamoleException { + + // Retrieve all possible user preference attributes + return userContext.getUserPreferenceAttributes(); + + } + /** * Retrieves the possible attributes of a user group object. * diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java index f31ce5dc8..5d4be4ffc 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java @@ -21,6 +21,11 @@ package org.apache.guacamole.rest.user; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; + +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; + import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -145,9 +150,51 @@ public class UserResource @Override public void updateObject(APIUser modifiedObject) throws GuacamoleException { - // A user may not use this endpoint to modify himself - if (userContext.self().getIdentifier().equals(modifiedObject.getUsername())) - throw new GuacamoleSecurityException("Permission denied."); + User currentUser = userContext.self(); + + // A user may not use this endpoint to modify themself, except in the case + // that they are modifying one of the user attributes explicitly exposed + // in the user preferences form + if (currentUser.getIdentifier().equals(modifiedObject.getUsername())) { + + // A user may not use this endpoint to update their password + if (currentUser.getPassword() != null) + throw new GuacamoleSecurityException( + "Permission denied. The password update endpoint must" + + " be used to change the current user's password."); + + // All attributes exposed in the preferences forms + Set preferenceAttributes = ( + userContext.getUserPreferenceAttributes().stream() + .flatMap(form -> form.getFields().stream().map( + field -> field.getName()))) + .collect(Collectors.toSet()); + + // Go through every attribute value and check if it's changed + Iterator keyIterator = modifiedObject.getAttributes().keySet().iterator(); + while(keyIterator.hasNext()) { + + String key = keyIterator.next(); + String newValue = modifiedObject.getAttributes().get(key); + + // If it's not a preference attribute, editing is not allowed + if (!preferenceAttributes.contains(key)) { + + String currentValue = currentUser.getAttributes().get(key); + + // If the value of the attribute has been modified + if ( + !(currentValue == null && newValue == null) && ( + (currentValue == null && newValue != null) || + !currentValue.equals(newValue) + ) + ) + throw new GuacamoleSecurityException( + "Permission denied. Only user preference attributes" + + " can be modified for the current user."); + } + } + } super.updateObject(modifiedObject); From 87cd7fbe2281beff0f86e7f210a7b059ebf7d798 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 4 Aug 2022 19:24:49 +0000 Subject: [PATCH 068/289] GUACAMOLE-1656: Remove user attributes header; it does not look good. --- .../java/org/apache/guacamole/vault/user/VaultUserContext.java | 2 +- .../src/app/settings/templates/settingsPreferences.html | 1 - guacamole/src/main/frontend/src/translations/en.json | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java index 748237dd2..eeddbe8d1 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-base/src/main/java/org/apache/guacamole/vault/user/VaultUserContext.java @@ -241,7 +241,7 @@ public class VaultUserContext extends TokenInjectingUserContext { * * @throws GuacamoleException * If the value for any applicable secret cannot be retrieved from the - * vault due to an error.1 + * vault due to an error. */ private Map> getTokens( Connectable connectable, Map tokenMapping, diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html index ac5cc4f59..6ecf6e0e7 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html @@ -90,7 +90,6 @@ -

{{'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_ATTRIBUTES' | translate}}

diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 7f52fea11..a1c0e0d92 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -951,7 +951,6 @@ "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Default Input Method", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Default Mouse Emulation Mode", - "SECTION_HEADER_UPDATE_ATTRIBUTES" : "User Attributes", "SECTION_HEADER_UPDATE_PASSWORD" : "Change Password" }, From 33f2b499efeefa09b912393b09364bc010f1b87d Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 5 Aug 2022 00:01:12 +0000 Subject: [PATCH 069/289] GUACAMOLE-1656: Fall back to user KSM config for single value fetch. --- .../vault/ksm/GuacamoleExceptionSupplier.java | 42 +++++++++++ .../guacamole/vault/ksm/secret/KsmClient.java | 42 ++++++++++- .../vault/ksm/secret/KsmSecretService.java | 72 ++++++++++++++++--- 3 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java new file mode 100644 index 000000000..d966a7320 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java @@ -0,0 +1,42 @@ +/* + * 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.vault.ksm; + +import org.apache.guacamole.GuacamoleException; + +/** + * A class that is basically equivalent to the standard Supplier class in + * Java, except that the get() function can throw GuacamoleException, which + * is impossible with any of the standard Java lambda type classes, since + * none of them can handle checked exceptions + */ +public abstract class GuacamoleExceptionSupplier { + + /** + * Returns a value of the declared type. + * + * @return + * A value of the declared type. + * + * @throws GuacamoleException + * If an error occurs while attemping to calculate the return value. + */ + public abstract T get() throws GuacamoleException; +} \ No newline at end of file diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index 6e0bdf177..74a27e472 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -34,7 +34,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -45,10 +44,12 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nullable; + import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.net.auth.User; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.secret.WindowsUsername; +import org.apache.guacamole.vault.ksm.GuacamoleExceptionSupplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -595,6 +596,38 @@ public class KsmClient { * is invalid. */ public Future getSecret(String notation) throws GuacamoleException { + return getSecret(notation, null); + } + + /** + * Returns the value of the secret stored within Keeper Secrets Manager and + * represented by the given Keeper notation. Keeper notation locates the + * value of a specific field, custom field, or file associated with a + * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation + * If a fallbackFunction is provided, it will be invoked to generate + * a return value in the case where no secrest is found with the given + * keeper notation. + * + * @param notation + * The Keeper notation of the secret to retrieve. + * + * @param fallbackFunction + * A function to invoke in order to produce a Future for return, + * if the requested secret is not found. If the provided Function + * is null, it will not be run. + * + * @return + * A Future which completes with the value of the secret represented by + * the given Keeper notation, or null if there is no such secret. + * + * @throws GuacamoleException + * If the requested secret cannot be retrieved or the Keeper notation + * is invalid. + */ + public Future getSecret( + String notation, + @Nullable GuacamoleExceptionSupplier> fallbackFunction) + throws GuacamoleException { validateCache(); cacheLock.readLock().lock(); try { @@ -614,6 +647,11 @@ public class KsmClient { catch (Error e) { logger.warn("Record \"{}\" does not exist.", notation); logger.debug("Retrieval of record by Keeper notation failed.", e); + + // If the secret is not found, invoke the fallback function + if (fallbackFunction != null) + return fallbackFunction.get(); + return CompletableFuture.completedFuture(null); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index a3b772256..6b8ba92a2 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -48,6 +48,7 @@ import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.protocol.GuacamoleConfiguration; import org.apache.guacamole.token.TokenFilter; +import org.apache.guacamole.vault.ksm.GuacamoleExceptionSupplier; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.secret.VaultSecretService; @@ -147,7 +148,24 @@ public class KsmSecretService implements VaultSecretService { // Attempt to find a KSM config for this connection or group String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable); - return getClient(ksmConfig).getSecret(name); + return getClient(ksmConfig).getSecret(name, new GuacamoleExceptionSupplier>() { + + @Override + public Future get() throws GuacamoleException { + + // Get the user-supplied KSM config, if allowed by config and + // set by the user + String userKsmConfig = getUserKSMConfig(userContext, connectable); + + // If the user config happens to be the same as admin-defined one, + // don't bother trying again + if (userKsmConfig != ksmConfig) + return getClient(userKsmConfig).getSecret(name); + + return CompletableFuture.completedFuture(null); + } + + }); } @Override @@ -335,6 +353,44 @@ public class KsmSecretService implements VaultSecretService { } + /** + * Return the KSM config blob for the current user IFF user KSM configs + * are enabled globally, and are enabled for the given connectable. If no + * KSM config exists for the given user or KSM configs are not enabled, + * null will be returned. + * + * @param userContext + * The user context from which the current user should be fetched. + * + * @param connectable + * The connectable to which the connection is being established. This + * is the conneciton which will be checked to see if user KSM configs + * are enabled. + * + * @return + * The base64 encoded KSM config blob for the current user if one + * exists, and if user KSM configs are enabled globally and for the + * provided connectable. + * + * @throws GuacamoleException + * If an error occurs while attempting to fetch the KSM config. + */ + private String getUserKSMConfig( + UserContext userContext, Connectable connectable) throws GuacamoleException { + + // Check if user KSM configs are enabled globally, and for the given connectable + if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable)) + + // Return the user-specific KSM config, if one exists + return userContext.self().getAttributes().get( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + + + // If user-specific KSM config is disabled globally or for the given + // connectable, return null to indicate that no user config exists + return null; + } + @Override public Map> getTokens(UserContext userContext, Connectable connectable, GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException { @@ -351,16 +407,9 @@ public class KsmSecretService implements VaultSecretService { // Only use the user-specific KSM config if explicitly enabled in the global // configuration, AND for the specific connectable being connected to - if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable)) { - - // Find a user-specific KSM config, if one exists - String userKsmConfig = userContext.self().getAttributes().get( - KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); - - // If a user-specific config exsts, process it first - if (userKsmConfig != null && !userKsmConfig.trim().isEmpty()) - ksmClients.add(0, getClient(userKsmConfig)); - } + String userKsmConfig = getUserKSMConfig(userContext, connectable); + if (userKsmConfig != null && !userKsmConfig.trim().isEmpty()) + ksmClients.add(0, getClient(userKsmConfig)); // Iterate through the KSM clients, processing using the user-specific // config first (if it exists), to ensure that any admin-defined values @@ -431,6 +480,7 @@ public class KsmSecretService implements VaultSecretService { addRecordTokens(tokens, "KEEPER_USER_", ksm.getRecordByLogin(filter.filter(username), null)); } + } return tokens; From e882a08486badd527a615ff38fce8bc3520b12de Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 5 Aug 2022 15:56:09 +0000 Subject: [PATCH 070/289] GUACAMOLE-1656: Ensure the preferences page refreshes on save in case a one-time-token was updated. --- .../directives/guacSettingsPreferences.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js index 5824eb194..199b7ddfa 100644 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js +++ b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js @@ -58,6 +58,20 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe } }; + /** + * An action which closes the current dialog, and refreshes + * the user data on dialog close. + */ + const ACKNOWLEDGE_ACTION_RELOAD = { + name : 'SETTINGS_PREFERENCES.ACTION_ACKNOWLEDGE', + // Handle action + callback : function acknowledgeCallback() { + userService.getUser(dataSource, username) + .then(user => $scope.user = user) + .then(guacNotification.showStatus(false)) + } + }; + /** * The user being modified. * @@ -237,7 +251,9 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe text : { key : 'SETTINGS_PREFERENCES.INFO_PREFERENCE_ATTRIBUTES_CHANGED' }, - actions : [ ACKNOWLEDGE_ACTION ] + + // Reload the user on successful save in case any attributes changed + actions : [ ACKNOWLEDGE_ACTION_RELOAD ] }), guacNotification.SHOW_REQUEST_ERROR); }; From 3790d76fc9485f5229632d4583976a593679abd3 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 5 Aug 2022 18:30:22 +0000 Subject: [PATCH 071/289] GUACAMOLE-1656: Force refresh the user context on updateUserContext to ensure that any modified user attributes are picked up. --- .../JDBCAuthenticationProviderService.java | 45 ++++++++++++++++--- .../guacamole/auth/jdbc/user/UserService.java | 12 +++-- .../vault/ksm/GuacamoleExceptionSupplier.java | 1 + .../controllers/manageUserController.js | 2 - 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java index d2576ec2b..e276b2766 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java @@ -89,9 +89,31 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider } - @Override - public ModeledUserContext getUserContext(AuthenticationProvider authenticationProvider, - AuthenticatedUser authenticatedUser) throws GuacamoleException { + /** + * Gets a user context for the given authentication provider and user. If + * forceRefresh is set to true, the user record will be re-fetched even if + * it has already been loaded from the database. If not, the existing + * user will be used. + * + * @param authenticationProvider + * The authentication provider to use when loading or refreshing the user. + * + * @param authenticatedUser + * The user for which the user context is being fetched. + * + * @param forceRefresh + * A flag that, when set to true, will force the authenticated user to + * refreshed from the database. If false, an existing DB user will be + * reused. + * + * @return + * The fetched user context. + * + * @throws GuacamoleException + * If an error occurs while fetching or refreshing the user context. + */ + private ModeledUserContext getUserContext(AuthenticationProvider authenticationProvider, + AuthenticatedUser authenticatedUser, boolean forceRefresh) throws GuacamoleException { // Always allow but provide no data for users authenticated via our own // connection sharing links @@ -102,8 +124,9 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider boolean databaseCredentialsUsed = (authenticatedUser instanceof ModeledAuthenticatedUser); boolean databaseRestrictionsApplicable = (databaseCredentialsUsed || environment.isUserRequired()); - // Retrieve user account for already-authenticated user - ModeledUser user = userService.retrieveUser(authenticationProvider, authenticatedUser); + // Retrieve user account for already-authenticated user, forcing a refresh if requested + ModeledUser user = userService.retrieveUser( + authenticationProvider, authenticatedUser, forceRefresh); ModeledUserContext context = userContextProvider.get(); if (user != null && !user.isDisabled()) { @@ -159,13 +182,21 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider } + @Override + public ModeledUserContext getUserContext(AuthenticationProvider authenticationProvider, + AuthenticatedUser authenticatedUser) throws GuacamoleException { + + // Do not force refresh unless updateUserContext is explicitly called + return getUserContext(authenticationProvider, authenticatedUser, false); + } + @Override public UserContext updateUserContext(AuthenticationProvider authenticationProvider, UserContext context, AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { - // No need to update the context - return context; + // Force-refresh the user context + return getUserContext(authenticationProvider, authenticatedUser, true); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java index c43aa1b01..3d23e64b0 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java @@ -404,6 +404,11 @@ public class UserService extends ModeledDirectoryObjectService { * If an error occurs while attemping to calculate the return value. */ public abstract T get() throws GuacamoleException; + } \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js b/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js index 94e3d8944..f7ead136a 100644 --- a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js +++ b/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js @@ -501,6 +501,4 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto return userService.deleteUser($scope.dataSource, $scope.user); }; - console.log($scope); - }]); From dfc7e6dd90bffacc6f0e52ed818da33bed63dcf2 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 8 Aug 2022 21:38:45 +0000 Subject: [PATCH 072/289] GUACAMOLE-1656: Simplify auto-refresh behavior in JDBC auth provider. --- .../JDBCAuthenticationProviderService.java | 45 +++---------------- .../guacamole/auth/jdbc/user/UserService.java | 12 +---- 2 files changed, 8 insertions(+), 49 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java index e276b2766..2b130b610 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java @@ -89,31 +89,9 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider } - /** - * Gets a user context for the given authentication provider and user. If - * forceRefresh is set to true, the user record will be re-fetched even if - * it has already been loaded from the database. If not, the existing - * user will be used. - * - * @param authenticationProvider - * The authentication provider to use when loading or refreshing the user. - * - * @param authenticatedUser - * The user for which the user context is being fetched. - * - * @param forceRefresh - * A flag that, when set to true, will force the authenticated user to - * refreshed from the database. If false, an existing DB user will be - * reused. - * - * @return - * The fetched user context. - * - * @throws GuacamoleException - * If an error occurs while fetching or refreshing the user context. - */ - private ModeledUserContext getUserContext(AuthenticationProvider authenticationProvider, - AuthenticatedUser authenticatedUser, boolean forceRefresh) throws GuacamoleException { + @Override + public ModeledUserContext getUserContext(AuthenticationProvider authenticationProvider, + AuthenticatedUser authenticatedUser) throws GuacamoleException { // Always allow but provide no data for users authenticated via our own // connection sharing links @@ -124,9 +102,8 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider boolean databaseCredentialsUsed = (authenticatedUser instanceof ModeledAuthenticatedUser); boolean databaseRestrictionsApplicable = (databaseCredentialsUsed || environment.isUserRequired()); - // Retrieve user account for already-authenticated user, forcing a refresh if requested - ModeledUser user = userService.retrieveUser( - authenticationProvider, authenticatedUser, forceRefresh); + // Retrieve user account for already-authenticated user + ModeledUser user = userService.retrieveUser(authenticationProvider, authenticatedUser); ModeledUserContext context = userContextProvider.get(); if (user != null && !user.isDisabled()) { @@ -182,21 +159,13 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider } - @Override - public ModeledUserContext getUserContext(AuthenticationProvider authenticationProvider, - AuthenticatedUser authenticatedUser) throws GuacamoleException { - - // Do not force refresh unless updateUserContext is explicitly called - return getUserContext(authenticationProvider, authenticatedUser, false); - } - @Override public UserContext updateUserContext(AuthenticationProvider authenticationProvider, UserContext context, AuthenticatedUser authenticatedUser, Credentials credentials) throws GuacamoleException { - // Force-refresh the user context - return getUserContext(authenticationProvider, authenticatedUser, true); + // Refresh the user context + return getUserContext(authenticationProvider, authenticatedUser); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java index 3d23e64b0..161976ce4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java @@ -404,11 +404,6 @@ public class UserService extends ModeledDirectoryObjectService Date: Mon, 12 Sep 2022 09:14:12 -0700 Subject: [PATCH 073/289] GUACAMOLE-990: Guarantee auth banning occurs before all other auth processing. --- .../ban/BanningAuthenticationListener.java | 148 ++++++++++++-- .../ban/BanningAuthenticationProvider.java | 182 ------------------ .../src/main/resources/guac-manifest.json | 4 - guacamole-docker/bin/start.sh | 12 +- .../AuthenticationRequestReceivedEvent.java | 35 ++++ .../rest/auth/AuthenticationService.java | 4 + 6 files changed, 178 insertions(+), 207 deletions(-) delete mode 100644 extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationRequestReceivedEvent.java diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java index 4d8a3bbad..c1ba8a569 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java @@ -21,41 +21,158 @@ package org.apache.guacamole.auth.ban; import org.apache.guacamole.auth.ban.status.AuthenticationFailureTracker; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.ban.status.InMemoryAuthenticationFailureTracker; +import org.apache.guacamole.auth.ban.status.NullAuthenticationFailureTracker; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.event.AuthenticationFailureEvent; import org.apache.guacamole.net.event.AuthenticationSuccessEvent; import org.apache.guacamole.net.event.listener.Listener; +import org.apache.guacamole.net.event.AuthenticationRequestReceivedEvent; +import org.apache.guacamole.properties.IntegerGuacamoleProperty; +import org.apache.guacamole.properties.LongGuacamoleProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Listener implementation which automatically tracks authentication failures - * such that further authentication attempts may be automatically blocked by - * {@link BanningAuthenticationProvider} if they match configured criteria. + * such that further authentication attempts may be automatically blocked if + * they match configured criteria. */ public class BanningAuthenticationListener implements Listener { /** - * Shared tracker of addresses that have repeatedly failed authentication. + * Logger for this class. */ - private static AuthenticationFailureTracker tracker; + private static final Logger logger = LoggerFactory.getLogger(BanningAuthenticationListener.class); /** - * Assigns the shared tracker instance used by both the {@link BanningAuthenticationProvider} - * and this listener. This function MUST be invoked with the tracker - * created for BanningAuthenticationProvider as soon as possible (during - * construction of BanningAuthenticationProvider), or processing of - * received events will fail internally. - * - * @param tracker - * The tracker instance to use for received authentication events. + * The maximum number of failed authentication attempts allowed before an + * address is temporarily banned. */ - public static void setAuthenticationFailureTracker(AuthenticationFailureTracker tracker) { - BanningAuthenticationListener.tracker = tracker; + private static final IntegerGuacamoleProperty MAX_ATTEMPTS = new IntegerGuacamoleProperty() { + + @Override + public String getName() { + return "ban-max-invalid-attempts"; + } + + }; + + /** + * The length of time that each address should be banned after reaching the + * maximum number of failed authentication attempts, in seconds. + */ + private static final IntegerGuacamoleProperty IP_BAN_DURATION = new IntegerGuacamoleProperty() { + + @Override + public String getName() { + return "ban-address-duration"; + } + + }; + + /** + * The maximum number of failed authentication attempts tracked at any + * given time. Once this number of addresses is exceeded, the oldest + * authentication attempts are rotated off on an LRU basis. + */ + private static final LongGuacamoleProperty MAX_ADDRESSES = new LongGuacamoleProperty() { + + @Override + public String getName() { + return "ban-max-addresses"; + } + + }; + + /** + * The default maximum number of failed authentication attempts allowed + * before an address is temporarily banned. + */ + private static final int DEFAULT_MAX_ATTEMPTS = 5; + + /** + * The default length of time that each address should be banned after + * reaching the maximum number of failed authentication attempts, in + * seconds. + */ + private static final int DEFAULT_IP_BAN_DURATION = 300; + + /** + * The maximum number of failed authentication attempts tracked at any + * given time. Once this number of addresses is exceeded, the oldest + * authentication attempts are rotated off on an LRU basis. + */ + private static final long DEFAULT_MAX_ADDRESSES = 10485760; + + /** + * Tracker of addresses that have repeatedly failed authentication. + */ + private final AuthenticationFailureTracker tracker; + + /** + * Creates a new BanningAuthenticationListener which automatically bans + * further authentication attempts from addresses that have repeatedly + * failed to authenticate. The ban duration and maximum number of failed + * attempts allowed before banning are configured within + * guacamole.properties. + * + * @throws GuacamoleException + * If an error occurs parsing the configuration properties used by this + * extension. + */ + public BanningAuthenticationListener() throws GuacamoleException { + + Environment environment = LocalEnvironment.getInstance(); + int maxAttempts = environment.getProperty(MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS); + int banDuration = environment.getProperty(IP_BAN_DURATION, DEFAULT_IP_BAN_DURATION); + long maxAddresses = environment.getProperty(MAX_ADDRESSES, DEFAULT_MAX_ADDRESSES); + + // Configure auth failure tracking behavior and inform administrator of + // ultimate result + if (maxAttempts <= 0) { + this.tracker = new NullAuthenticationFailureTracker(); + logger.info("Maximum failed authentication attempts has been set " + + "to {}. Automatic banning of brute-force authentication " + + "attempts will be disabled.", maxAttempts); + } + else if (banDuration <= 0) { + this.tracker = new NullAuthenticationFailureTracker(); + logger.info("Ban duration for addresses that repeatedly fail " + + "authentication has been set to {}. Automatic banning " + + "of brute-force authentication attempts will be " + + "disabled.", banDuration); + } + else if (maxAddresses <= 0) { + this.tracker = new NullAuthenticationFailureTracker(); + logger.info("Maximum number of tracked addresses has been set to " + + "{}. Automatic banning of brute-force authentication " + + "attempts will be disabled.", maxAddresses); + } + else { + this.tracker = new InMemoryAuthenticationFailureTracker(maxAttempts, banDuration, maxAddresses); + logger.info("Addresses will be automatically banned for {} " + + "seconds after {} failed authentication attempts. Up " + + "to {} unique addresses will be tracked/banned at any " + + "given time.", banDuration, maxAttempts, maxAddresses); + } + } @Override public void handleEvent(Object event) throws GuacamoleException { - if (event instanceof AuthenticationFailureEvent) { + // Notify auth tracker of each request received BEFORE the request is + // processed ... + if (event instanceof AuthenticationRequestReceivedEvent) { + AuthenticationRequestReceivedEvent request = (AuthenticationRequestReceivedEvent) event; + tracker.notifyAuthenticationRequestReceived(request.getCredentials()); + } + + // ... as well as every explicit failure ... + else if (event instanceof AuthenticationFailureEvent) { AuthenticationFailureEvent failure = (AuthenticationFailureEvent) event; @@ -72,6 +189,7 @@ public class BanningAuthenticationListener implements Listener { } + // ... and explicit success. else if (event instanceof AuthenticationSuccessEvent) { AuthenticationSuccessEvent success = (AuthenticationSuccessEvent) event; tracker.notifyAuthenticationSuccess(success.getCredentials()); diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java deleted file mode 100644 index 1d115d39d..000000000 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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.ban; - -import org.apache.guacamole.auth.ban.status.InMemoryAuthenticationFailureTracker; -import org.apache.guacamole.auth.ban.status.AuthenticationFailureTracker; -import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.ban.status.NullAuthenticationFailureTracker; -import org.apache.guacamole.environment.Environment; -import org.apache.guacamole.environment.LocalEnvironment; -import org.apache.guacamole.net.auth.AbstractAuthenticationProvider; -import org.apache.guacamole.net.auth.AuthenticatedUser; -import org.apache.guacamole.net.auth.Credentials; -import org.apache.guacamole.net.auth.UserContext; -import org.apache.guacamole.properties.IntegerGuacamoleProperty; -import org.apache.guacamole.properties.LongGuacamoleProperty; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * AuthenticationProvider implementation that blocks further authentication - * attempts that are related to past authentication failures flagged by - * {@link BanningAuthenticationListener}. - */ -public class BanningAuthenticationProvider extends AbstractAuthenticationProvider { - - /** - * Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger(BanningAuthenticationProvider.class); - - /** - * The maximum number of failed authentication attempts allowed before an - * address is temporarily banned. - */ - private static final IntegerGuacamoleProperty MAX_ATTEMPTS = new IntegerGuacamoleProperty() { - - @Override - public String getName() { - return "ban-max-invalid-attempts"; - } - - }; - - /** - * The length of time that each address should be banned after reaching the - * maximum number of failed authentication attempts, in seconds. - */ - private static final IntegerGuacamoleProperty IP_BAN_DURATION = new IntegerGuacamoleProperty() { - - @Override - public String getName() { - return "ban-address-duration"; - } - - }; - - /** - * The maximum number of failed authentication attempts tracked at any - * given time. Once this number of addresses is exceeded, the oldest - * authentication attempts are rotated off on an LRU basis. - */ - private static final LongGuacamoleProperty MAX_ADDRESSES = new LongGuacamoleProperty() { - - @Override - public String getName() { - return "ban-max-addresses"; - } - - }; - - /** - * The default maximum number of failed authentication attempts allowed - * before an address is temporarily banned. - */ - private static final int DEFAULT_MAX_ATTEMPTS = 5; - - /** - * The default length of time that each address should be banned after - * reaching the maximum number of failed authentication attempts, in - * seconds. - */ - private static final int DEFAULT_IP_BAN_DURATION = 300; - - /** - * The maximum number of failed authentication attempts tracked at any - * given time. Once this number of addresses is exceeded, the oldest - * authentication attempts are rotated off on an LRU basis. - */ - private static final long DEFAULT_MAX_ADDRESSES = 10485760; - - /** - * Shared tracker of addresses that have repeatedly failed authentication. - */ - private final AuthenticationFailureTracker tracker; - - /** - * Creates a new BanningAuthenticationProvider which automatically bans - * further authentication attempts from addresses that have repeatedly - * failed to authenticate. The ban duration and maximum number of failed - * attempts allowed before banning are configured within - * guacamole.properties. - * - * @throws GuacamoleException - * If an error occurs parsing the configuration properties used by this - * extension. - */ - public BanningAuthenticationProvider() throws GuacamoleException { - - Environment environment = LocalEnvironment.getInstance(); - int maxAttempts = environment.getProperty(MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS); - int banDuration = environment.getProperty(IP_BAN_DURATION, DEFAULT_IP_BAN_DURATION); - long maxAddresses = environment.getProperty(MAX_ADDRESSES, DEFAULT_MAX_ADDRESSES); - - // Configure auth failure tracking behavior and inform administrator of - // ultimate result - if (maxAttempts <= 0) { - this.tracker = new NullAuthenticationFailureTracker(); - logger.info("Maximum failed authentication attempts has been set " - + "to {}. Automatic banning of brute-force authentication " - + "attempts will be disabled.", maxAttempts); - } - else if (banDuration <= 0) { - this.tracker = new NullAuthenticationFailureTracker(); - logger.info("Ban duration for addresses that repeatedly fail " - + "authentication has been set to {}. Automatic banning " - + "of brute-force authentication attempts will be " - + "disabled.", banDuration); - } - else if (maxAddresses <= 0) { - this.tracker = new NullAuthenticationFailureTracker(); - logger.info("Maximum number of tracked addresses has been set to " - + "{}. Automatic banning of brute-force authentication " - + "attempts will be disabled.", maxAddresses); - } - else { - this.tracker = new InMemoryAuthenticationFailureTracker(maxAttempts, banDuration, maxAddresses); - logger.info("Addresses will be automatically banned for {} " - + "seconds after {} failed authentication attempts. Up " - + "to {} unique addresses will be tracked/banned at any " - + "given time.", banDuration, maxAttempts, maxAddresses); - } - - BanningAuthenticationListener.setAuthenticationFailureTracker(tracker); - - } - - @Override - public String getIdentifier() { - return "ban"; - } - - @Override - public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException { - tracker.notifyAuthenticationRequestReceived(credentials); - return null; - } - - @Override - public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException { - tracker.notifyAuthenticationRequestReceived(authenticatedUser.getCredentials()); - return null; - } - -} diff --git a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json index 1e6beac22..0133a7364 100644 --- a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json @@ -5,10 +5,6 @@ "name" : "Brute-force Authentication Detection/Prevention", "namespace" : "ban", - "authProviders" : [ - "org.apache.guacamole.auth.ban.BanningAuthenticationProvider" - ], - "listeners" : [ "org.apache.guacamole.auth.ban.BanningAuthenticationListener" ], diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh index 632c3d03a..7ea97bf73 100755 --- a/guacamole-docker/bin/start.sh +++ b/guacamole-docker/bin/start.sh @@ -1165,12 +1165,12 @@ set_optional_property "ban-address-duration" "$BAN_ADDRESS_DURATION" set_optional_property "ban-max-addresses" "$BAN_MAX_ADDRESSES" set_optional_property "ban-max-invalid-attempts" "$BAN_MAX_INVALID_ATTEMPTS" -# Ensure guacamole-auth-ban always loads before other extensions unless -# explicitly overridden via naming or EXTENSION_PRIORITY (allowing other -# extensions to attempt authentication before guacamole-auth-ban has a chance -# to enforce any bans could allow credentials to continue to be guessed even -# after the address has been blocked via timing attacks) -ln -s /opt/guacamole/ban/guacamole-auth-*.jar "$GUACAMOLE_EXT/_guacamole-auth-ban.jar" +# Always load guacamole-auth-ban extension (automatic banning can be disabled +# through seting BAN_ADDRESS_DURATION to 0). As guacamole-auth-ban performs +# its banning by handling a pre-authentication event, it is guaranteed to +# perform its checks before all other auth processing and load order does not +# matter. +ln -s /opt/guacamole/ban/guacamole-auth-*.jar "$GUACAMOLE_EXT" # Set logback level if specified if [ -n "$LOGBACK_LEVEL" ]; then diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationRequestReceivedEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationRequestReceivedEvent.java new file mode 100644 index 000000000..eeefcfc5b --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationRequestReceivedEvent.java @@ -0,0 +1,35 @@ +/* + * 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.event; + +/** + * An event which is triggered whenever a user's credentials have been + * submitted for authentication, but that latest authentication request has not + * yet succeeded or failed. The credentials that were received for + * authentication are included within this event, and can be retrieved using + * {@link #getCredentials()}. + *

+ * If a {@link org.apache.guacamole.net.event.listener.Listener} throws + * a GuacamoleException when handling an event of this type, the authentication + * request is entirely aborted as if it failed, and will be processed by any + * other listener or authentication provider. + */ +public interface AuthenticationRequestReceivedEvent extends CredentialEvent { +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java index 8ed76c6a6..e7248ba51 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java @@ -38,6 +38,7 @@ import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.apache.guacamole.net.event.AuthenticationFailureEvent; +import org.apache.guacamole.net.event.AuthenticationRequestReceivedEvent; import org.apache.guacamole.net.event.AuthenticationSuccessEvent; import org.apache.guacamole.rest.event.ListenerService; import org.glassfish.jersey.server.ContainerRequest; @@ -412,6 +413,9 @@ public class AuthenticationService { public String authenticate(Credentials credentials, String token) throws GuacamoleException { + // Fire pre-authentication event before ANY authn/authz occurs at all + listenerService.handleEvent((AuthenticationRequestReceivedEvent) () -> credentials); + // Pull existing session if token provided GuacamoleSession existingSession; if (token != null) From 8a7bde8e9ce4630640ae73cfedb4b9ae61f709e2 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 29 Aug 2022 22:08:31 +0000 Subject: [PATCH 074/289] GUACAMOLE-1656: Simplify, clean up, and improve documentation of KSM code. --- .../vault/ksm/GuacamoleExceptionSupplier.java | 8 +- .../vault/ksm/conf/KsmAttributeService.java | 22 +- .../guacamole/vault/ksm/secret/KsmClient.java | 2 +- .../vault/ksm/secret/KsmSecretService.java | 205 ++++++++++-------- .../vault/ksm/user/KsmConnection.java | 29 +-- .../vault/ksm/user/KsmConnectionGroup.java | 17 +- .../vault/ksm/user/KsmDirectoryService.java | 29 +-- .../guacamole/vault/ksm/user/KsmUser.java | 29 +-- .../directives/guacSettingsPreferences.js | 70 +----- .../main/frontend/src/translations/en.json | 2 +- guacamole/src/main/frontend/webpack.config.js | 12 + .../guacamole/rest/schema/SchemaResource.java | 2 +- .../rest/user/UserObjectTranslator.java | 19 +- .../guacamole/rest/user/UserResource.java | 54 +---- 14 files changed, 216 insertions(+), 284 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java index ed59f7b68..c99613740 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/GuacamoleExceptionSupplier.java @@ -26,8 +26,12 @@ import org.apache.guacamole.GuacamoleException; * Java, except that the get() function can throw GuacamoleException, which * is impossible with any of the standard Java lambda type classes, since * none of them can handle checked exceptions + * + * @param + * The type of object which will be returned as a result of calling + * get(). */ -public abstract class GuacamoleExceptionSupplier { +public interface GuacamoleExceptionSupplier { /** * Returns a value of the declared type. @@ -38,6 +42,6 @@ public abstract class GuacamoleExceptionSupplier { * @throws GuacamoleException * If an error occurs while attemping to calculate the return value. */ - public abstract T get() throws GuacamoleException; + public T get() throws GuacamoleException; } \ No newline at end of file diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java index bdceb4c21..24958208d 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java @@ -28,6 +28,8 @@ import org.apache.guacamole.form.BooleanField; import org.apache.guacamole.form.Form; import org.apache.guacamole.form.TextField; import org.apache.guacamole.vault.conf.VaultAttributeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -39,7 +41,14 @@ import com.google.inject.Singleton; @Singleton public class KsmAttributeService implements VaultAttributeService { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(KsmAttributeService.class); + /** + * Service for retrieving KSM configuration details. + */ @Inject private KsmConfigurationService configurationService; @@ -57,7 +66,7 @@ public class KsmAttributeService implements VaultAttributeService { Arrays.asList(new TextField(KSM_CONFIGURATION_ATTRIBUTE))); /** - * All KSM-specific attributes for users or connection groups, organized by form. + * All KSM-specific attributes for users, connections, or connection groups, organized by form. */ public static final Collection KSM_ATTRIBUTES = Collections.unmodifiableCollection(Arrays.asList(KSM_CONFIGURATION_FORM)); @@ -108,7 +117,16 @@ public class KsmAttributeService implements VaultAttributeService { // Expose the user attributes IFF user-level KSM configuration is enabled return configurationService.getAllowUserConfig() ? KSM_ATTRIBUTES : Collections.emptyList(); - } catch (GuacamoleException e) { + } + + catch (GuacamoleException e) { + + logger.warn( + "Unable to determine if user preference attributes " + + "should be exposed due to config parsing error: {}.", e.getMessage()); + logger.debug( + "Config parsing error prevented checking user preference configuration", + e); // If the configuration can't be parsed, default to not exposing the attribute return Collections.emptyList(); diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index 74a27e472..3aa436f69 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -605,7 +605,7 @@ public class KsmClient { * value of a specific field, custom field, or file associated with a * specific record. See: https://docs.keeper.io/secrets-manager/secrets-manager/about/keeper-notation * If a fallbackFunction is provided, it will be invoked to generate - * a return value in the case where no secrest is found with the given + * a return value in the case where no secret is found with the given * keeper notation. * * @param notation diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 6b8ba92a2..59c1450bf 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -19,6 +19,7 @@ package org.apache.guacamole.vault.ksm.secret; +import com.google.common.base.Objects; import com.google.inject.Inject; import com.google.inject.Singleton; import com.keepersecurity.secretsManager.core.KeeperRecord; @@ -41,6 +42,7 @@ import java.util.concurrent.Future; import javax.annotation.Nonnull; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.Attributes; import org.apache.guacamole.net.auth.Connectable; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; @@ -159,7 +161,7 @@ public class KsmSecretService implements VaultSecretService { // If the user config happens to be the same as admin-defined one, // don't bother trying again - if (userKsmConfig != ksmConfig) + if (!Objects.equal(userKsmConfig, ksmConfig)) return getClient(userKsmConfig).getSecret(name); return CompletableFuture.completedFuture(null); @@ -339,16 +341,12 @@ public class KsmSecretService implements VaultSecretService { */ private boolean isKsmUserConfigEnabled(Connectable connectable) { - // If it's a connection, user-level config is enabled IFF the appropriate - // attribute is set to true - if (connectable instanceof Connection) - return KsmAttributeService.TRUTH_VALUE.equals(((Connection) connectable).getAttributes().get( + // User-level config is enabled IFF the appropriate attribute is set to true + if (connectable instanceof Attributes) + return KsmAttributeService.TRUTH_VALUE.equals(((Attributes) connectable).getAttributes().get( KsmAttributeService.KSM_USER_CONFIG_ENABLED_ATTRIBUTE)); - // KSM token replacement is not enabled for balancing groups, so for - // now, user-level KSM configs will be explicitly disabled. - // TODO: If token replacement is implemented for balancing groups, - // implement this functionality for them as well. + // If there's no attributes to check, the user config cannot be enabled return false; } @@ -378,10 +376,10 @@ public class KsmSecretService implements VaultSecretService { private String getUserKSMConfig( UserContext userContext, Connectable connectable) throws GuacamoleException { - // Check if user KSM configs are enabled globally, and for the given connectable + // If user KSM configs are enabled globally, and for the given connectable, + // return the user-specific KSM config, if one exists if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable)) - // Return the user-specific KSM config, if one exists return userContext.self().getAttributes().get( KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); @@ -391,6 +389,106 @@ public class KsmSecretService implements VaultSecretService { return null; } + /** + * Use the provided KSM client to add parameter tokens tokens to the + * provided token map. The supplied filter will be used to replace + * existing tokens in the provided connection parameters before KSM + * record lookup. The supplied GuacamoleConfiguration instance will + * be used to check the protocol, in case RDP-specific behavior is + * needed. + + * @param config + * The GuacamoleConfiguration associated with the Connectable for which + * tokens are being added. + * + * @param ksm + * The KSM client to use when fetching records. + * + * @param tokens + * The tokens to which any fetched KSM record values should be added. + * + * @param parameters + * The connection parameters associated with the Connectable for which + * tokens are being added. + * + * @throws GuacamoleException + * If an error occurs while attempting to fetch KSM records or check + * configuration settings. + */ + private void addConnectableTokens( + GuacamoleConfiguration config, KsmClient ksm, Map> tokens, + Map parameters, TokenFilter filter) throws GuacamoleException { + + // Retrieve and define server-specific tokens, if any + String hostname = parameters.get("hostname"); + if (hostname != null && !hostname.isEmpty()) + addRecordTokens(tokens, "KEEPER_SERVER_", + ksm.getRecordByHost(filter.filter(hostname))); + + // Tokens specific to RDP + if ("rdp".equals(config.getProtocol())) { + + // Retrieve and define gateway server-specific tokens, if any + String gatewayHostname = parameters.get("gateway-hostname"); + if (gatewayHostname != null && !gatewayHostname.isEmpty()) + addRecordTokens(tokens, "KEEPER_GATEWAY_", + ksm.getRecordByHost(filter.filter(gatewayHostname))); + + // Retrieve and define domain tokens, if any + String domain = parameters.get("domain"); + String filteredDomain = null; + if (domain != null && !domain.isEmpty()) { + filteredDomain = filter.filter(domain); + addRecordTokens(tokens, "KEEPER_DOMAIN_", + ksm.getRecordByDomain(filteredDomain)); + } + + // Retrieve and define gateway domain tokens, if any + String gatewayDomain = parameters.get("gateway-domain"); + String filteredGatewayDomain = null; + if (gatewayDomain != null && !gatewayDomain.isEmpty()) { + filteredGatewayDomain = filter.filter(gatewayDomain); + addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_", + ksm.getRecordByDomain(filteredGatewayDomain)); + } + + // If domain matching is disabled for user records, + // explicitly set the domains to null when storing + // user records to enable username-only matching + if (!confService.getMatchUserRecordsByDomain()) { + filteredDomain = null; + filteredGatewayDomain = null; + } + + // Retrieve and define user-specific tokens, if any + String username = parameters.get("username"); + if (username != null && !username.isEmpty()) + addRecordTokens(tokens, "KEEPER_USER_", + ksm.getRecordByLogin(filter.filter(username), + filteredDomain)); + + // Retrieve and define gateway user-specific tokens, if any + String gatewayUsername = parameters.get("gateway-username"); + if (gatewayUsername != null && !gatewayUsername.isEmpty()) + addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", + ksm.getRecordByLogin( + filter.filter(gatewayUsername), + filteredGatewayDomain)); + } + + else { + + // Retrieve and define user-specific tokens, if any + // NOTE that non-RDP connections do not have a domain + // field in the connection parameters, so the domain + // will always be null + String username = parameters.get("username"); + if (username != null && !username.isEmpty()) + addRecordTokens(tokens, "KEEPER_USER_", + ksm.getRecordByLogin(filter.filter(username), null)); + } + } + @Override public Map> getTokens(UserContext userContext, Connectable connectable, GuacamoleConfiguration config, TokenFilter filter) throws GuacamoleException { @@ -398,89 +496,18 @@ public class KsmSecretService implements VaultSecretService { Map> tokens = new HashMap<>(); Map parameters = config.getParameters(); - // Attempt to find a KSM config for this connection or group - String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable); - - // Create a list containing just the global / connection group config - List ksmClients = new ArrayList<>(2); - ksmClients.add(getClient(ksmConfig)); - // Only use the user-specific KSM config if explicitly enabled in the global // configuration, AND for the specific connectable being connected to String userKsmConfig = getUserKSMConfig(userContext, connectable); if (userKsmConfig != null && !userKsmConfig.trim().isEmpty()) - ksmClients.add(0, getClient(userKsmConfig)); + addConnectableTokens( + config, getClient(userKsmConfig), tokens, parameters, filter); - // Iterate through the KSM clients, processing using the user-specific - // config first (if it exists), to ensure that any admin-defined values - // will override the user-speicifc values - Iterator ksmIterator = ksmClients.iterator(); - while (ksmIterator.hasNext()) { - - KsmClient ksm = ksmIterator.next(); - - // Retrieve and define server-specific tokens, if any - String hostname = parameters.get("hostname"); - if (hostname != null && !hostname.isEmpty()) - addRecordTokens(tokens, "KEEPER_SERVER_", - ksm.getRecordByHost(filter.filter(hostname))); - - // Tokens specific to RDP - if ("rdp".equals(config.getProtocol())) { - // Retrieve and define domain tokens, if any - String domain = parameters.get("domain"); - String filteredDomain = null; - if (domain != null && !domain.isEmpty()) { - filteredDomain = filter.filter(domain); - addRecordTokens(tokens, "KEEPER_DOMAIN_", - ksm.getRecordByDomain(filteredDomain)); - } - - // Retrieve and define gateway domain tokens, if any - String gatewayDomain = parameters.get("gateway-domain"); - String filteredGatewayDomain = null; - if (gatewayDomain != null && !gatewayDomain.isEmpty()) { - filteredGatewayDomain = filter.filter(gatewayDomain); - addRecordTokens(tokens, "KEEPER_GATEWAY_DOMAIN_", - ksm.getRecordByDomain(filteredGatewayDomain)); - } - - // If domain matching is disabled for user records, - // explicitly set the domains to null when storing - // user records to enable username-only matching - if (!confService.getMatchUserRecordsByDomain()) { - filteredDomain = null; - filteredGatewayDomain = null; - } - - // Retrieve and define user-specific tokens, if any - String username = parameters.get("username"); - if (username != null && !username.isEmpty()) - addRecordTokens(tokens, "KEEPER_USER_", - ksm.getRecordByLogin(filter.filter(username), - filteredDomain)); - - // Retrieve and define gateway user-specific tokens, if any - String gatewayUsername = parameters.get("gateway-username"); - if (gatewayUsername != null && !gatewayUsername.isEmpty()) - addRecordTokens(tokens, "KEEPER_GATEWAY_USER_", - ksm.getRecordByLogin( - filter.filter(gatewayUsername), - filteredGatewayDomain)); - } - - else { - - // Retrieve and define user-specific tokens, if any - // NOTE that non-RDP connections do not have a domain - // field in the connection parameters, so the domain - // will always be null - String username = parameters.get("username"); - if (username != null && !username.isEmpty()) - addRecordTokens(tokens, "KEEPER_USER_", - ksm.getRecordByLogin(filter.filter(username), null)); - } - } + // Add connection group or globally defined tokens after the user-specific + // ones to ensure that the user config will be overriden on collision + String ksmConfig = getConnectionGroupKsmConfig(userContext, connectable); + addConnectableTokens( + config, getClient(ksmConfig), tokens, parameters, filter); return tokens; diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java index da2866202..24d7dd062 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java @@ -19,25 +19,21 @@ package org.apache.guacamole.vault.ksm.user; -import java.util.List; import java.util.Map; import org.apache.guacamole.net.auth.DelegatingConnection; +import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.net.auth.Connection; import com.google.common.collect.Maps; /** - * A Connection that explicitly adds a blank entry for any defined - * KSM connection attributes. + * A Connection that explicitly adds a blank entry for any defined KSM + * connection attributes. This ensures that any such field will always + * be displayed to the user when editing a connection through the UI. */ public class KsmConnection extends DelegatingConnection { - /** - * The names of all connection attributes defined for the vault. - */ - private List connectionAttributeNames; - /** * Create a new Vault connection wrapping the provided Connection record. Any * attributes defined in the provided connection attribute forms will have empty @@ -45,15 +41,9 @@ public class KsmConnection extends DelegatingConnection { * * @param connection * The connection record to wrap. - * - * @param connectionAttributeNames - * The names of all connection attributes to automatically expose. */ - KsmConnection(Connection connection, List connectionAttributeNames) { - + KsmConnection(Connection connection) { super(connection); - this.connectionAttributeNames = connectionAttributeNames; - } /** @@ -70,13 +60,12 @@ public class KsmConnection extends DelegatingConnection { public Map getAttributes() { // Make a copy of the existing map - Map attributeMap = Maps.newHashMap(super.getAttributes()); + Map attributes = Maps.newHashMap(super.getAttributes()); - // Add every defined attribute - connectionAttributeNames.forEach( - attributeName -> attributeMap.putIfAbsent(attributeName, null)); + // Add the configuration attribute + attributes.putIfAbsent(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); + return attributes; - return attributeMap; } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java index 397c42f11..175e4dd5f 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java @@ -19,13 +19,14 @@ package org.apache.guacamole.vault.ksm.user; -import java.util.HashMap; import java.util.Map; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.DelegatingConnectionGroup; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; +import com.google.common.collect.Maps; + /** * A KSM-specific connection group implementation that always exposes * the KSM_CONFIGURATION_ATTRIBUTE attribute, even when no value is set. @@ -50,17 +51,11 @@ import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; @Override public Map getAttributes() { - // All attributes defined on the underlying connection group - Map attributes = super.getAttributes(); + // Make a copy of the existing map + Map attributes = Maps.newHashMap(super.getAttributes()); - // If the attribute is already present, there's no need to add it - return - // the existing attributes as they are - if (attributes.containsKey(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE)) - return attributes; - - // Make a copy of the existing attributes and add KSM_CONFIGURATION_ATTRIBUTE - attributes = new HashMap<>(attributes); - attributes.put(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); + // Add the configuration attribute + attributes.putIfAbsent(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); return attributes; } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java index 3eff928fe..6ce734601 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java @@ -26,7 +26,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.language.TranslatableGuacamoleClientException; @@ -60,12 +59,6 @@ public class KsmDirectoryService extends VaultDirectoryService { @Inject private KsmConfigurationService configurationService; - /** - * Service for retrieving KSM-specific attributes. - */ - @Inject - private KsmAttributeService ksmAttributeService; - /** * A singleton ObjectMapper for converting a Map to a JSON string when * generating a base64-encoded JSON KSM config blob. @@ -235,8 +228,9 @@ public class KsmDirectoryService extends VaultDirectoryService { public Directory getConnectionDirectory( Directory underlyingDirectory) throws GuacamoleException { - // A Connection directory that will intercept add and update calls to - // validate KSM configurations, and translate one-time-tokens, if possible + // A Connection directory that will decorate all connections with a + // KsmConnection wrapper to ensure that all defined KSM fields will + // be exposed in the connection group attributes. return new DecoratingDirectory(underlyingDirectory) { @Override @@ -244,17 +238,13 @@ public class KsmDirectoryService extends VaultDirectoryService { // Wrap in a KsmConnection class to ensure that all defined KSM fields will be // present - return new KsmConnection( - connection, - ksmAttributeService.getConnectionAttributes().stream().flatMap( - form -> form.getFields().stream().map(field -> field.getName()) - ).collect(Collectors.toList())); + return new KsmConnection(connection); } @Override protected Connection undecorate(Connection connection) throws GuacamoleException { - // Unwrap the KsmUser + // Unwrap the KsmConnection return ((KsmConnection) connection).getUnderlyingConnection(); } @@ -315,6 +305,9 @@ public class KsmDirectoryService extends VaultDirectoryService { // A User directory that will intercept add and update calls to // validate KSM configurations, and translate one-time-tokens, if possible + // Additionally, this directory will will decorate all users with a + // KsmUser wrapper to ensure that all defined KSM fields will be exposed + // in the user attributes. return new DecoratingDirectory(underlyingDirectory) { @Override @@ -340,11 +333,7 @@ public class KsmDirectoryService extends VaultDirectoryService { // Wrap in a KsmUser class to ensure that all defined KSM fields will be // present - return new KsmUser( - user, - ksmAttributeService.getUserAttributes().stream().flatMap( - form -> form.getFields().stream().map(field -> field.getName()) - ).collect(Collectors.toList())); + return new KsmUser(user); } @Override diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java index a846eb0d8..dc2877564 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java @@ -19,11 +19,11 @@ package org.apache.guacamole.vault.ksm.user; -import java.util.List; import java.util.Map; import org.apache.guacamole.net.auth.DelegatingUser; import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import com.google.common.collect.Maps; @@ -33,27 +33,16 @@ import com.google.common.collect.Maps; */ public class KsmUser extends DelegatingUser { - /** - * The names of all user attributes defined for the vault. - */ - private List userAttributeNames; - /** * Create a new Vault user wrapping the provided User record. Any - * attributes defined in the provided user attribute forms will have empty - * values automatically populated when getAttributes() is called. + * KSM-specific attribute forms will have empty values automatically + * populated when getAttributes() is called. * * @param user * The user record to wrap. - * - * @param userAttributeNames - * The names of all user attributes to automatically expose. */ - KsmUser(User user, List userAttributeNames) { - + KsmUser(User user) { super(user); - this.userAttributeNames = userAttributeNames; - } /** @@ -70,13 +59,11 @@ public class KsmUser extends DelegatingUser { public Map getAttributes() { // Make a copy of the existing map - Map attributeMap = Maps.newHashMap(super.getAttributes()); + Map attributes = Maps.newHashMap(super.getAttributes()); - // Add every defined attribute - userAttributeNames.forEach( - attributeName -> attributeMap.putIfAbsent(attributeName, null)); - - return attributeMap; + // Add the configuration attribute + attributes.putIfAbsent(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); + return attributes; } } diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js index 199b7ddfa..a3f0fd2e5 100644 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js +++ b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js @@ -68,7 +68,7 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe callback : function acknowledgeCallback() { userService.getUser(dataSource, username) .then(user => $scope.user = user) - .then(guacNotification.showStatus(false)) + .then(() => guacNotification.showStatus(false)); } }; @@ -101,17 +101,6 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe */ $scope.preferences = preferenceService.preferences; - /** - * All available user attributes, as a mapping of form name to form - * object. The form object contains a name, as well as a Map of fields. - * - * The Map type is used here to maintain form/name uniqueness, as well as - * insertion order, to ensure a consistent UI experience. - * - * @type Map - */ - $scope.attributeMap = new Map(); - /** * All available user attributes. This is only the set of attribute * definitions, organized as logical groupings of attributes, not attribute @@ -263,61 +252,10 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe $scope.user = user; }) - // Get all datasources that are available for this user - authenticationService.getAvailableDataSources().forEach(function loadAttributesForDataSource(dataSource) { - - // Fetch all user attribute forms defined for the datasource - schemaService.getUserPreferenceAttributes(dataSource).then(function saveAttributes(attributes) { - - // Iterate through all attribute forms - attributes.forEach(function addAttribute(attributeForm) { - - // If the form with the retrieved name already exists - if ($scope.attributeMap.has(attributeForm.name)) { - const existingFields = $scope.attributeMap.get(attributeForm.name).fields; - - // Add each field to the existing list for this form - attributeForm.fields.forEach(function addAllFieldsToExistingMap(field) { - existingFields.set(field.name, field); - }) - } - - else { - - // Create a new entry for the form - $scope.attributeMap.set(attributeForm.name, { - name: attributeForm.name, - - // With the field array from the API converted into a Map - fields: attributeForm.fields.reduce( - function addFieldToMap(currentFieldMap, field) { - currentFieldMap.set(field.name, field); - return currentFieldMap; - }, new Map() - ) - - }) - } - - }); - - // Re-generate the attributes array every time - $scope.attributes = Array.of(...$scope.attributeMap.values()).map(function convertFieldsToArray(formObject) { - - // Convert each temporary form object to a Form type - return new Form({ - name: formObject.name, - - // Convert the field map to a simple array of fields - fields: Array.of(...formObject.fields.values()) - }) - }); - - }); - + // Fetch all user preference attribute forms defined + schemaService.getUserPreferenceAttributes(dataSource).then(function saveAttributes(attributes) { + $scope.attributes = attributes; }); - }] }; - }]); diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index a1c0e0d92..f2ba0b111 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -943,7 +943,7 @@ "HELP_UPDATE_PASSWORD" : "If you wish to change your password, enter your current password and the desired new password below, and click \"Update Password\". The change will take effect immediately.", "INFO_PASSWORD_CHANGED" : "Password changed.", - "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "User attributes saved.", + "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "User settings saved.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index 3f1574fa1..29bb8ddd5 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -77,6 +77,18 @@ module.exports = { ] }, optimization: { + minimizer: [ + + // Minify using Google Closure Compiler + new ClosureWebpackPlugin({ mode: 'STANDARD' }, { + languageIn: 'ECMASCRIPT_2020', + languageOut: 'ECMASCRIPT5', + compilationLevel: 'SIMPLE' + }), + + new CssMinimizerPlugin() + + ], splitChunks: { cacheGroups: { diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java index edc125127..a7ed08d60 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/schema/SchemaResource.java @@ -89,7 +89,7 @@ public class SchemaResource { */ @GET @Path("userPreferenceAttributes") - public Collection getUserAttrigetUserPreferenceAttributesbutes() + public Collection getUserPreferenceAttributes() throws GuacamoleException { // Retrieve all possible user preference attributes diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserObjectTranslator.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserObjectTranslator.java index 8536b3507..8c63d7218 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserObjectTranslator.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserObjectTranslator.java @@ -59,9 +59,22 @@ public class UserObjectTranslator public void filterExternalObject(UserContext userContext, APIUser object) throws GuacamoleException { - // Filter object attributes by defined schema - object.setAttributes(filterAttributes(userContext.getUserAttributes(), - object.getAttributes())); + // If a user is editing themselves ... + if (object.getUsername().equals(userContext.self().getIdentifier())) { + + // ... they may only edit preference attributes + object.setAttributes(filterAttributes(userContext.getUserPreferenceAttributes(), + object.getAttributes())); + + } + + else { + + // In all other cases, filter object attributes by defined schema + object.setAttributes(filterAttributes(userContext.getUserAttributes(), + object.getAttributes())); + + } } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java index 5d4be4ffc..aa8a3ec7e 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java @@ -22,10 +22,6 @@ package org.apache.guacamole.rest.user; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; -import java.util.Iterator; -import java.util.Set; -import java.util.stream.Collectors; - import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -150,50 +146,14 @@ public class UserResource @Override public void updateObject(APIUser modifiedObject) throws GuacamoleException { + // A user may not use this endpoint to update their password User currentUser = userContext.self(); - - // A user may not use this endpoint to modify themself, except in the case - // that they are modifying one of the user attributes explicitly exposed - // in the user preferences form - if (currentUser.getIdentifier().equals(modifiedObject.getUsername())) { - - // A user may not use this endpoint to update their password - if (currentUser.getPassword() != null) - throw new GuacamoleSecurityException( - "Permission denied. The password update endpoint must" - + " be used to change the current user's password."); - - // All attributes exposed in the preferences forms - Set preferenceAttributes = ( - userContext.getUserPreferenceAttributes().stream() - .flatMap(form -> form.getFields().stream().map( - field -> field.getName()))) - .collect(Collectors.toSet()); - - // Go through every attribute value and check if it's changed - Iterator keyIterator = modifiedObject.getAttributes().keySet().iterator(); - while(keyIterator.hasNext()) { - - String key = keyIterator.next(); - String newValue = modifiedObject.getAttributes().get(key); - - // If it's not a preference attribute, editing is not allowed - if (!preferenceAttributes.contains(key)) { - - String currentValue = currentUser.getAttributes().get(key); - - // If the value of the attribute has been modified - if ( - !(currentValue == null && newValue == null) && ( - (currentValue == null && newValue != null) || - !currentValue.equals(newValue) - ) - ) - throw new GuacamoleSecurityException( - "Permission denied. Only user preference attributes" - + " can be modified for the current user."); - } - } + if ( + currentUser.getIdentifier().equals(modifiedObject.getUsername()) + && modifiedObject.getPassword() != null) { + throw new GuacamoleSecurityException( + "Permission denied. The password update endpoint must" + + " be used to change the current user's password."); } super.updateObject(modifiedObject); From b7deee427d60182bb77c930d488f8a63c72b26f4 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 26 Sep 2022 10:32:38 -0700 Subject: [PATCH 075/289] GUACAMOLE-990: Ensure internal errors during auth reach global error handling/logging. --- .../rest/auth/AuthenticationService.java | 13 ++++++++ ...acamoleAuthenticationProcessException.java | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java index e7248ba51..705d7a856 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java @@ -27,6 +27,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleUnauthorizedException; import org.apache.guacamole.GuacamoleSession; import org.apache.guacamole.environment.Environment; @@ -480,6 +481,18 @@ public class AuthenticationService { getLoggableAddress(request)); // Rethrow exception + e.rethrowCause(); + + // This line SHOULD be unreachable unless a bug causes + // rethrowCause() to not actually rethrow the underlying failure + Throwable cause = e.getCause(); + if (cause != null) { + logger.warn("An underlying internal error was not correctly rethrown by rethrowCause(): {}", cause.getMessage()); + logger.debug("Internal error not rethrown by rethrowCause().", cause); + } + else + logger.warn("An underlying internal error was not correctly rethrown by rethrowCause()."); + throw e.getCauseAsGuacamoleException(); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java index ba1d72a50..bec73f0be 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java @@ -146,6 +146,38 @@ public class GuacamoleAuthenticationProcessException extends GuacamoleException return guacCause; } + /** + * Rethrows the original GuacamoleException wrapped within this + * GuacamoleAuthenticationProcessException. If there is no such exception, + * and the cause of this failure is an unchecked RuntimeException or Error, + * that unchecked exception/error is rethrown as-is. + * + * @throws GuacamoleException + * If the underlying cause of this exception is a checked + * GuacamoleException subclass. + * + * @throws RuntimeException + * If the underlying cause of this exception is an unchecked + * RuntimeException. + * + * @throws Error + * If the underlying cause of this exception is an unchecked Error. + */ + public void rethrowCause() throws GuacamoleException, RuntimeException, Error { + + // Rethrow any unchecked exceptions/errors as-is + Throwable cause = getCause(); + if (cause instanceof RuntimeException) + throw (RuntimeException) cause; + if (cause instanceof Error) + throw (Error) cause; + + // Pass through all other exceptions as normal GuacamoleException + // subclassses + throw getCauseAsGuacamoleException(); + + } + @Override public GuacamoleStatus getStatus() { return getCauseAsGuacamoleException().getStatus(); From 3036c96a1f4bd2995d1b53f641cf32883e6796cd Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 27 Sep 2022 16:46:25 -0700 Subject: [PATCH 076/289] GUACAMOLE-1687: Leverage network activity to ensure keep-alive pings are sent. --- .../src/main/webapp/modules/Client.js | 94 +++++++++++++++++-- .../src/main/webapp/modules/Tunnel.js | 50 +++++++--- 2 files changed, 124 insertions(+), 20 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 2cc9c28b1..2315ac306 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -42,7 +42,36 @@ Guacamole.Client = function(tunnel) { var currentState = STATE_IDLE; var currentTimestamp = 0; - var pingInterval = null; + + /** + * The rough number of milliseconds to wait between sending keep-alive + * pings. This may vary depending on how frequently the browser allows + * timers to run, as well as how frequently the client receives messages + * from the server. + * + * @private + * @constant + * @type {!number} + */ + var KEEP_ALIVE_FREQUENCY = 5000; + + /** + * The current keep-alive ping timeout ID, if any. This will only be set + * upon connecting. + * + * @private + * @type {number} + */ + var keepAliveTimeout = null; + + /** + * The timestamp of the point in time that the last keep-live ping was + * sent, in milliseconds elapsed since midnight of January 1, 1970 UTC. + * + * @private + * @type {!number} + */ + var lastSentKeepAlive = 0; /** * Translation from Guacamole protocol line caps to Layer line caps. @@ -1649,12 +1678,63 @@ Guacamole.Client = function(tunnel) { }; + /** + * Sends a keep-alive ping to the Guacamole server, advising the server + * that the client is still connected and responding. The lastSentKeepAlive + * timestamp is automatically updated as a result of calling this function. + * + * @private + */ + var sendKeepAlive = function sendKeepAlive() { + tunnel.sendMessage('nop'); + lastSentKeepAlive = new Date().getTime(); + }; + + /** + * Schedules the next keep-alive ping based on the KEEP_ALIVE_FREQUENCY and + * the time that the last ping was sent, if ever. If enough time has + * elapsed that a ping should have already been sent, calling this function + * will send that ping immediately. + * + * @private + */ + var scheduleKeepAlive = function scheduleKeepAlive() { + + window.clearTimeout(keepAliveTimeout); + + var currentTime = new Date().getTime(); + var keepAliveDelay = Math.max(lastSentKeepAlive + KEEP_ALIVE_FREQUENCY - currentTime, 0); + + // Ping server regularly to keep connection alive, but send the ping + // immediately if enough time has elapsed that it should have already + // been sent + if (keepAliveDelay > 0) + keepAliveTimeout = window.setTimeout(sendKeepAlive, keepAliveDelay); + else + sendKeepAlive(); + + }; + + /** + * Stops sending any further keep-alive pings. If a keep-alive ping was + * scheduled to be sent, that ping is cancelled. + * + * @private + */ + var stopKeepAlive = function stopKeepAlive() { + window.clearTimeout(keepAliveTimeout); + }; + tunnel.oninstruction = function(opcode, parameters) { var handler = instructionHandlers[opcode]; if (handler) handler(parameters); + // Leverage network activity to ensure the next keep-alive ping is + // sent, even if the browser is currently throttling timers + scheduleKeepAlive(); + }; /** @@ -1668,9 +1748,8 @@ Guacamole.Client = function(tunnel) { setState(STATE_DISCONNECTING); - // Stop ping - if (pingInterval) - window.clearInterval(pingInterval); + // Stop sending keep-alive messages + stopKeepAlive(); // Send disconnect message and disconnect tunnel.sendMessage("disconnect"); @@ -1704,10 +1783,9 @@ Guacamole.Client = function(tunnel) { throw status; } - // Ping every 5 seconds (ensure connection alive) - pingInterval = window.setInterval(function() { - tunnel.sendMessage("nop"); - }, 5000); + // Regularly send keep-alive ping to ensure the server knows we're + // still here, even if not active + scheduleKeepAlive(); setState(STATE_WAITING); }; diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index fa886f561..2ff97d60d 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -844,13 +844,13 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { var unstableTimeout = null; /** - * The current connection stability test ping interval ID, if any. This + * The current connection stability test ping timeout ID, if any. This * will only be set upon successful connection. * * @private * @type {number} */ - var pingInterval = null; + var pingTimeout = null; /** * The WebSocket protocol corresponding to the protocol used for the current @@ -874,6 +874,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { */ var PING_FREQUENCY = 500; + /** + * The timestamp of the point in time that the last connection stability + * test ping was sent, in milliseconds elapsed since midnight of January 1, + * 1970 UTC. + * + * @private + * @type {!number} + */ + var lastSentPing = 0; + // Transform current URL to WebSocket URL // If not already a websocket URL @@ -907,6 +917,20 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { } + /** + * Sends an internal "ping" instruction to the Guacamole WebSocket + * endpoint, verifying network connection stability. If the network is + * stable, the Guacamole server will receive this instruction and respond + * with an identical ping. + * + * @private + */ + var sendPing = function sendPing() { + var currentTime = new Date().getTime(); + tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime); + lastSentPing = currentTime; + }; + /** * Initiates a timeout which, if data is not received, causes the tunnel * to close with an error. @@ -918,6 +942,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { // Get rid of old timeouts (if any) window.clearTimeout(receive_timeout); window.clearTimeout(unstableTimeout); + window.clearTimeout(pingTimeout); // Clear unstable status if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE) @@ -933,6 +958,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); }, tunnel.unstableThreshold); + var currentTime = new Date().getTime(); + var pingDelay = Math.max(lastSentPing + PING_FREQUENCY - currentTime, 0); + + // Ping tunnel endpoint regularly to test connection stability, sending + // the ping immediately if enough time has already elapsed + if (pingDelay > 0) + pingTimeout = window.setTimeout(sendPing, pingDelay); + else + sendPing(); + } /** @@ -949,9 +984,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { // Get rid of old timeouts (if any) window.clearTimeout(receive_timeout); window.clearTimeout(unstableTimeout); - - // Cease connection test pings - window.clearInterval(pingInterval); + window.clearTimeout(pingTimeout); // Ignore if already closed if (tunnel.state === Guacamole.Tunnel.State.CLOSED) @@ -1020,13 +1053,6 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { socket.onopen = function(event) { reset_timeout(); - - // Ping tunnel endpoint regularly to test connection stability - pingInterval = setInterval(function sendPing() { - tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, - "ping", new Date().getTime()); - }, PING_FREQUENCY); - }; socket.onclose = function(event) { From 7f2ea222bba635b8ccb24316a8675b11343df386 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 28 Sep 2022 10:50:25 -0700 Subject: [PATCH 077/289] GUACAMOLE-1687: Clean, document, and clarify usage of tunnel timer reset function. --- .../src/main/webapp/modules/Tunnel.js | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index 2ff97d60d..5f3b79f3c 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -356,12 +356,16 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { } /** - * Initiates a timeout which, if data is not received, causes the tunnel - * to close with an error. - * + * Resets the state of timers tracking network activity and stability. If + * those timers are not yet started, invoking this function starts them. + * This function should be invoked when the tunnel is established and every + * time there is network activity on the tunnel, such that the timers can + * safely assume the network and/or server are not responding if this + * function has not been invoked for a significant period of time. + * * @private */ - function reset_timeout() { + var resetTimers = function resetTimers() { // Get rid of old timeouts (if any) window.clearTimeout(receive_timeout); @@ -381,7 +385,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); }, tunnel.unstableThreshold); - } + }; /** * Closes this tunnel, signaling the given status and corresponding @@ -491,7 +495,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { message_xmlhttprequest.onreadystatechange = function() { if (message_xmlhttprequest.readyState === 4) { - reset_timeout(); + resetTimers(); // If an error occurs during send, handle it if (message_xmlhttprequest.status !== 200) @@ -581,7 +585,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { if (xmlhttprequest.readyState === 3 || xmlhttprequest.readyState === 4) { - reset_timeout(); + resetTimers(); // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) if (pollingMode === POLLING_ENABLED) { @@ -742,7 +746,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { this.connect = function(data) { // Start waiting for connect - reset_timeout(); + resetTimers(); // Mark the tunnel as connecting tunnel.setState(Guacamole.Tunnel.State.CONNECTING); @@ -760,7 +764,7 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { return; } - reset_timeout(); + resetTimers(); // Get UUID and HTTP-specific tunnel session token from response tunnel.setUUID(connect_xmlhttprequest.responseText); @@ -932,12 +936,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { }; /** - * Initiates a timeout which, if data is not received, causes the tunnel - * to close with an error. - * + * Resets the state of timers tracking network activity and stability. If + * those timers are not yet started, invoking this function starts them. + * This function should be invoked when the tunnel is established and every + * time there is network activity on the tunnel, such that the timers can + * safely assume the network and/or server are not responding if this + * function has not been invoked for a significant period of time. + * * @private */ - function reset_timeout() { + var resetTimers = function resetTimers() { // Get rid of old timeouts (if any) window.clearTimeout(receive_timeout); @@ -968,7 +976,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { else sendPing(); - } + }; /** * Closes this tunnel, signaling the given status and corresponding @@ -1043,7 +1051,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { this.connect = function(data) { - reset_timeout(); + resetTimers(); // Mark the tunnel as connecting tunnel.setState(Guacamole.Tunnel.State.CONNECTING); @@ -1052,7 +1060,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); socket.onopen = function(event) { - reset_timeout(); + resetTimers(); }; socket.onclose = function(event) { @@ -1074,7 +1082,7 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { socket.onmessage = function(event) { - reset_timeout(); + resetTimers(); var message = event.data; var startIndex = 0; From 06d321fe5dde0cfebee17ef6d21f8267a078ddff Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 27 Sep 2022 17:09:25 +0000 Subject: [PATCH 078/289] GUCAMOLE-1656: Do not expose the KSM config blob through the REST API. --- .../ksm/KsmAuthenticationProviderModule.java | 8 + .../vault/ksm/conf/KsmAttributeService.java | 215 ++++++++++++++ .../vault/ksm/secret/KsmSecretService.java | 19 +- .../vault/ksm/user/KsmConnection.java | 5 +- .../vault/ksm/user/KsmConnectionGroup.java | 49 ++-- .../vault/ksm/user/KsmDirectory.java | 93 ++++++ .../vault/ksm/user/KsmDirectoryService.java | 269 ++++-------------- .../guacamole/vault/ksm/user/KsmUser.java | 74 ++++- .../vault/ksm/user/KsmUserFactory.java | 40 +++ 9 files changed, 519 insertions(+), 253 deletions(-) create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectory.java create mode 100644 extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUserFactory.java diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java index faf22aa4e..b9d38da93 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/KsmAuthenticationProviderModule.java @@ -24,7 +24,10 @@ import org.apache.guacamole.vault.VaultAuthenticationProviderModule; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmSecretService; +import org.apache.guacamole.vault.ksm.user.KsmConnectionGroup; import org.apache.guacamole.vault.ksm.user.KsmDirectoryService; +import org.apache.guacamole.vault.ksm.user.KsmUserFactory; +import org.apache.guacamole.vault.ksm.user.KsmUser; import org.apache.guacamole.vault.conf.VaultAttributeService; import org.apache.guacamole.vault.conf.VaultConfigurationService; import org.apache.guacamole.vault.ksm.secret.KsmClient; @@ -67,6 +70,11 @@ public class KsmAuthenticationProviderModule install(new FactoryModuleBuilder() .implement(KsmClient.class, KsmClient.class) .build(KsmClientFactory.class)); + + // Bind factory for creating KsmUsers + install(new FactoryModuleBuilder() + .implement(KsmUser.class, KsmUser.class) + .build(KsmUserFactory.class)); } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java index 24958208d..c1d3b95d5 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java @@ -19,20 +19,30 @@ package org.apache.guacamole.vault.ksm.conf; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.form.BooleanField; import org.apache.guacamole.form.Form; import org.apache.guacamole.form.TextField; +import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.vault.conf.VaultAttributeService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Inject; import com.google.inject.Singleton; +import com.keepersecurity.secretsManager.core.InMemoryStorage; +import com.keepersecurity.secretsManager.core.SecretsManager; +import com.keepersecurity.secretsManager.core.SecretsManagerOptions; /** * A service that exposes KSM-specific attributes, allowing setting KSM @@ -52,12 +62,41 @@ public class KsmAttributeService implements VaultAttributeService { @Inject private KsmConfigurationService configurationService; + /** + * A singleton ObjectMapper for converting a Map to a JSON string when + * generating a base64-encoded JSON KSM config blob. + */ + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * All expected fields in the KSM configuration JSON blob. + */ + private static final List EXPECTED_KSM_FIELDS = ( + Collections.unmodifiableList(Arrays.asList( + SecretsManager.KEY_HOSTNAME, + SecretsManager.KEY_CLIENT_ID, + SecretsManager.KEY_PRIVATE_KEY, + SecretsManager.KEY_CLIENT_KEY, + SecretsManager.KEY_APP_KEY, + SecretsManager.KEY_OWNER_PUBLIC_KEY, + SecretsManager.KEY_SERVER_PUBIC_KEY_ID + ))); + /** * The name of the attribute which can contain a KSM configuration blob * associated with either a connection group or user. */ public static final String KSM_CONFIGURATION_ATTRIBUTE = "ksm-config"; + /** + * The KSM configuration attribute contains sensitive information, so it + * should not be exposed through the directory. Instead, if a value is + * set on the attributes of an object, the following value will be exposed + * in its place, and correspondingly the underlying value will not be + * changed if this value is provided to an update call. + */ + public static final String KSM_ATTRIBUTE_PLACEHOLDER_VALUE = "**********"; + /** * All attributes related to configuring the KSM vault on a * per-connection-group or per-user basis. @@ -133,5 +172,181 @@ public class KsmAttributeService implements VaultAttributeService { } } + /** + * Sanitize the value of the provided KSM config attribute. If the provided + * config value is non-empty, it will be replaced with the placeholder + * value to avoid leaking sensitive information. If the value is empty, it + * will be replaced by `null`. + * + * @param ksmAttributeValue + * The KSM configuration attribute value to sanitize. + * + * @return + * The sanitized KSM configuration attribute value, stripped of any + * sensitive information. + */ + public static String sanitizeKsmAttributeValue(String ksmAttributeValue) { + + // Any non-empty values may contain sensitive information, and should + // be replaced by the safe placeholder value + if (ksmAttributeValue != null && !ksmAttributeValue.trim().isEmpty()) + return KSM_ATTRIBUTE_PLACEHOLDER_VALUE; + + // If the configuration value is empty, expose a null value + else + return null; + + } + + /** + * Return true if the provided input is a valid base64-encoded string, + * false otherwise. + * + * @param input + * The string to check if base-64 encoded. + * + * @return + * true if the provided input is a valid base64-encoded string, + * false otherwise. + */ + private static boolean isBase64(String input) { + + try { + Base64.getDecoder().decode(input); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * Given a map of attribute values, check for the presence of the + * KSM_CONFIGURATION_ATTRIBUTE attribute. If it's set, check if it's a valid + * KSM one-time token. If so, attempt to translate it to a base-64-encoded + * json KSM config blob. If it's already a KSM config blob, validate it as + * config blob. If either validation fails, a GuacamoleException will be thrown. + * The processed attribute values will be returned. + * + * @param attributes + * The attributes for which the KSM configuration attribute + * parsing/validation should be performed. + * + * @throws GuacamoleException + * If the KSM_CONFIGURATION_ATTRIBUTE is set, but fails to validate as + * either a KSM one-time-token, or a KSM base64-encoded JSON config blob. + */ + public Map processAttributes( + Map attributes) throws GuacamoleException { + + // Get the value of the KSM config attribute in the provided map + String ksmConfigValue = attributes.get( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + + // If the placeholder value was provided, do not update the attribute + if (KsmAttributeService.KSM_ATTRIBUTE_PLACEHOLDER_VALUE.equals(ksmConfigValue)) { + + // Remove the attribute from the map so it won't be updated + attributes = new HashMap<>(attributes); + attributes.remove(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + + } + + // Check if the attribute is set to a non-empty value + else if (ksmConfigValue != null && !ksmConfigValue.trim().isEmpty()) { + + // If it's already base64-encoded, it's a KSM configuration blob, + // so validate it immediately + if (isBase64(ksmConfigValue)) { + + // Attempt to validate the config as a base64-econded KSM config blob + try { + KsmConfig.parseKsmConfig(ksmConfigValue); + + // If it validates, the entity can be left alone - it's already valid + return attributes; + } + + catch (GuacamoleException exception) { + + // If the parsing attempt fails, throw a translatable error for display + // on the frontend + throw new TranslatableGuacamoleClientException( + "Invalid base64-encoded JSON KSM config provided for " + + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute", + "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_CONFIG_BLOB", + exception); + } + } + + // It wasn't a valid base64-encoded string, it should be a one-time token, so + // attempt to validat it as such, and if valid, update the attribute to the + // base64 config blob generated by the token + try { + + // Create an initially empty storage to be populated using the one-time token + InMemoryStorage storage = new InMemoryStorage(); + + // Populate the in-memory storage using the one-time-token + SecretsManager.initializeStorage(storage, ksmConfigValue, null); + + // Create an options object using the values we extracted from the one-time token + SecretsManagerOptions options = new SecretsManagerOptions( + storage, null, + configurationService.getAllowUnverifiedCertificate()); + + // Attempt to fetch secrets using the options we created. This will both validate + // that the configuration works, and potentially populate missing fields that the + // initializeStorage() call did not set. + SecretsManager.getSecrets(options); + + // Create a map to store the extracted values from the KSM storage + Map configMap = new HashMap<>(); + + // Go through all the expected fields, extract from the KSM storage, + // and write to the newly created map + EXPECTED_KSM_FIELDS.forEach(configKey -> { + + // Only write the value into the new map if non-null + String value = storage.getString(configKey); + if (value != null) + configMap.put(configKey, value); + + }); + + // JSON-encode the value, and then base64 encode that to get the format + // that KSM would expect + String jsonString = objectMapper.writeValueAsString(configMap); + String base64EncodedJson = Base64.getEncoder().encodeToString( + jsonString.getBytes(StandardCharsets.UTF_8)); + + // Finally, try to parse the newly generated token as a KSM config. If this + // works, the config should be fully functional + KsmConfig.parseKsmConfig(base64EncodedJson); + + // Make a copy of the existing attributes, modifying just the value for + // KSM_CONFIGURATION_ATTRIBUTE + attributes = new HashMap<>(attributes); + attributes.put( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, base64EncodedJson); + + } + + // The KSM SDK only throws raw Exceptions, so we can't be more specific + catch (Exception exception) { + + // If the parsing attempt fails, throw a translatable error for display + // on the frontend + throw new TranslatableGuacamoleClientException( + "Invalid one-time KSM token provided for " + + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute", + "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_ONE_TIME_TOKEN", + exception); + } + } + + return attributes; + + } + } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 59c1450bf..515c1fc3d 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -47,12 +47,14 @@ import org.apache.guacamole.net.auth.Connectable; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.protocol.GuacamoleConfiguration; import org.apache.guacamole.token.TokenFilter; import org.apache.guacamole.vault.ksm.GuacamoleExceptionSupplier; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; +import org.apache.guacamole.vault.ksm.user.KsmDirectory; import org.apache.guacamole.vault.secret.VaultSecretService; import org.apache.guacamole.vault.secret.WindowsUsername; import org.slf4j.Logger; @@ -299,7 +301,12 @@ public class KsmSecretService implements VaultSecretService { Set observedIdentifiers = new HashSet<>(); observedIdentifiers.add(parentIdentifier); - Directory connectionGroupDirectory = userContext.getConnectionGroupDirectory(); + // Use the unwrapped connection group directory to avoid KSM config + // value sanitization + Directory connectionGroupDirectory = ( + (KsmDirectory) userContext.getConnectionGroupDirectory() + ).getUnderlyingDirectory(); + while (true) { // Fetch the parent group, if one exists @@ -378,10 +385,16 @@ public class KsmSecretService implements VaultSecretService { // If user KSM configs are enabled globally, and for the given connectable, // return the user-specific KSM config, if one exists - if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable)) + if (confService.getAllowUserConfig() && isKsmUserConfigEnabled(connectable)) { - return userContext.self().getAttributes().get( + // Get the underlying user, to avoid the KSM config sanitization + User self = ( + ((KsmDirectory) userContext.getUserDirectory()) + .getUnderlyingDirectory().get(userContext.self().getIdentifier())); + + return self.getAttributes().get( KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + } // If user-specific KSM config is disabled globally or for the given diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java index 24d7dd062..3981486fe 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnection.java @@ -62,10 +62,9 @@ public class KsmConnection extends DelegatingConnection { // Make a copy of the existing map Map attributes = Maps.newHashMap(super.getAttributes()); - // Add the configuration attribute - attributes.putIfAbsent(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); + // Add the user-config-enabled configuration attribute + attributes.putIfAbsent(KsmAttributeService.KSM_USER_CONFIG_ENABLED_ATTRIBUTE, null); return attributes; - } } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java index 175e4dd5f..68d445771 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmConnectionGroup.java @@ -30,34 +30,31 @@ import com.google.common.collect.Maps; /** * A KSM-specific connection group implementation that always exposes * the KSM_CONFIGURATION_ATTRIBUTE attribute, even when no value is set. - * This ensures that the attribute will always show up in the UI, even - * for connection groups that don't already have it set. + * The value of the attribute will be sanitized if non-empty. This ensures + * that the attribute will always show up in the UI, even for connection + * groups that don't already have it set, and that any sensitive information + * in the attribute value will not be exposed. */ public class KsmConnectionGroup extends DelegatingConnectionGroup { /** - * Create a new KsmConnectionGroup instance, wrapping the provided - * ConnectionGroup. + * Create a new KsmConnectionGroup wrapping the provided ConnectionGroup record. * * @param connectionGroup - * The ConnectionGroup instance to wrap. + * The ConnectionGroup record to wrap. */ - public KsmConnectionGroup(ConnectionGroup connectionGroup) { - - // Wrap the provided connection group + KsmConnectionGroup(ConnectionGroup connectionGroup) { super(connectionGroup); } - @Override - public Map getAttributes() { - - // Make a copy of the existing map - Map attributes = Maps.newHashMap(super.getAttributes()); - - // Add the configuration attribute - attributes.putIfAbsent(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); - return attributes; - + /** + * Return the underlying wrapped connection group record. + * + * @return + * The wrapped connection group record. + */ + ConnectionGroup getUnderlyingConnectionGroup() { + return getDelegateConnectionGroup(); } /** @@ -70,4 +67,20 @@ import com.google.common.collect.Maps; return getDelegateConnectionGroup(); } + @Override + public Map getAttributes() { + + // Make a copy of the existing map + Map attributes = Maps.newHashMap(super.getAttributes()); + + // Sanitize the KSM configuration attribute, and ensure the attribute + // is always present + attributes.put( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, + KsmAttributeService.sanitizeKsmAttributeValue( + attributes.get(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE))); + + return attributes; + } + } \ No newline at end of file diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectory.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectory.java new file mode 100644 index 000000000..5cfcca947 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectory.java @@ -0,0 +1,93 @@ +/* + * 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.vault.ksm.user; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.DelegatingDirectory; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.Identifiable; + +/** + * A KSM-specific version of DecoratingDirectory that exposes the underlying + * directory for when it's needed. + */ +public abstract class KsmDirectory + extends DelegatingDirectory { + + /** + * Create a new KsmDirectory, delegating to the provided directory. + * + * @param directory + * The directory to delegate to. + */ + public KsmDirectory(Directory directory) { + super(directory); + } + + /** + * Returns the underlying directory that this DecoratingDirectory is + * delegating to. + * + * @return + * The underlying directory. + */ + public Directory getUnderlyingDirectory() { + return getDelegateDirectory(); + } + + /** + * Process and return a potentially-modified version of the object + * with the same identifier in the wrapped directory. + * + * @param object + * The object from the underlying directory. + * + * @return + * A potentially-modified version of the object with the same + * identifier in the wrapped directory. + */ + protected abstract ObjectType wrap(ObjectType object); + + @Override + public ObjectType get(String identifier) throws GuacamoleException { + + // Process and return the object from the wrapped directory + return wrap(super.get(identifier)); + + } + + @Override + public Collection getAll(Collection identifiers) + throws GuacamoleException { + + // Process and return each object from the wrapped directory + return super.getAll(identifiers).stream().map( + superObject -> wrap(superObject) + ).collect(Collectors.toList()); + + } + +} diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java index 6ce734601..5203a06dd 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java @@ -19,32 +19,17 @@ package org.apache.guacamole.vault.ksm.user; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.language.TranslatableGuacamoleClientException; -import org.apache.guacamole.net.auth.Attributes; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.DecoratingDirectory; +import org.apache.guacamole.net.auth.DelegatingDirectory; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; -import org.apache.guacamole.vault.ksm.conf.KsmConfig; -import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; import org.apache.guacamole.vault.user.VaultDirectoryService; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Inject; -import com.keepersecurity.secretsManager.core.InMemoryStorage; -import com.keepersecurity.secretsManager.core.SecretsManager; -import com.keepersecurity.secretsManager.core.SecretsManagerOptions; /** * A KSM-specific vault directory service that wraps the connection group directory @@ -54,175 +39,17 @@ import com.keepersecurity.secretsManager.core.SecretsManagerOptions; public class KsmDirectoryService extends VaultDirectoryService { /** - * Service for retrieving KSM configuration details. + * A factory for constructing new KsmUser instances. */ @Inject - private KsmConfigurationService configurationService; + private KsmUserFactory ksmUserFactory; /** - * A singleton ObjectMapper for converting a Map to a JSON string when - * generating a base64-encoded JSON KSM config blob. + * Service for retrieving any custom attributes defined for the + * current vault implementation and processing of said attributes. */ - private static final ObjectMapper objectMapper = new ObjectMapper(); - - /** - * All expected fields in the KSM configuration JSON blob. - */ - private static final List EXPECTED_KSM_FIELDS = ( - Collections.unmodifiableList(Arrays.asList( - SecretsManager.KEY_HOSTNAME, - SecretsManager.KEY_CLIENT_ID, - SecretsManager.KEY_PRIVATE_KEY, - SecretsManager.KEY_CLIENT_KEY, - SecretsManager.KEY_APP_KEY, - SecretsManager.KEY_OWNER_PUBLIC_KEY, - SecretsManager.KEY_SERVER_PUBIC_KEY_ID - ))); - - /** - * Return true if the provided input is a valid base64-encoded string, - * false otherwise. - * - * @param input - * The string to check if base-64 encoded. - * - * @return - * true if the provided input is a valid base64-encoded string, - * false otherwise. - */ - private static boolean isBase64(String input) { - - try { - Base64.getDecoder().decode(input); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - /** - * Given an attributes-enabled entity, check for the presence of the - * KSM_CONFIGURATION_ATTRIBUTE attribute. If it's set, check if it's a valid - * KSM one-time token. If so, attempt to translate it to a base-64-encoded - * json KSM config blob, and set it back to the provided entity. - * If it's already a KSM config blob, validate it as config blob. If either - * validation fails, a GuacamoleException will be thrown. - * - * @param entity - * The attributes-enabled entity for which the KSM configuration - * attribute parsing/validation should be performed. - * - * @throws GuacamoleException - * If the KSM_CONFIGURATION_ATTRIBUTE is set, but fails to validate as - * either a KSM one-time-token, or a KSM base64-encoded JSON config blob. - */ - public void processAttributes(Attributes entity) throws GuacamoleException { - - // By default, if the KSM config attribute isn't being set, pass the - // provided attributes through without any changes - Map attributes = entity.getAttributes(); - - // Get the value of the KSM config attribute in the provided map - String ksmConfigValue = attributes.get( - KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); - - // Check if the attribute is set to a non-empty value - if (ksmConfigValue != null && !ksmConfigValue.trim().isEmpty()) { - - // If it's already base64-encoded, it's a KSM configuration blob, - // so validate it immediately - if (isBase64(ksmConfigValue)) { - - // Attempt to validate the config as a base64-econded KSM config blob - try { - KsmConfig.parseKsmConfig(ksmConfigValue); - - // If it validates, the entity can be left alone - it's already valid - return; - } - - catch (GuacamoleException exception) { - - // If the parsing attempt fails, throw a translatable error for display - // on the frontend - throw new TranslatableGuacamoleClientException( - "Invalid base64-encoded JSON KSM config provided for " - + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute", - "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_CONFIG_BLOB", - exception); - } - } - - // It wasn't a valid base64-encoded string, it should be a one-time token, so - // attempt to validat it as such, and if valid, update the attribute to the - // base64 config blob generated by the token - try { - - // Create an initially empty storage to be populated using the one-time token - InMemoryStorage storage = new InMemoryStorage(); - - // Populate the in-memory storage using the one-time-token - SecretsManager.initializeStorage(storage, ksmConfigValue, null); - - // Create an options object using the values we extracted from the one-time token - SecretsManagerOptions options = new SecretsManagerOptions( - storage, null, - configurationService.getAllowUnverifiedCertificate()); - - // Attempt to fetch secrets using the options we created. This will both validate - // that the configuration works, and potentially populate missing fields that the - // initializeStorage() call did not set. - SecretsManager.getSecrets(options); - - // Create a map to store the extracted values from the KSM storage - Map configMap = new HashMap<>(); - - // Go through all the expected fields, extract from the KSM storage, - // and write to the newly created map - EXPECTED_KSM_FIELDS.forEach(configKey -> { - - // Only write the value into the new map if non-null - String value = storage.getString(configKey); - if (value != null) - configMap.put(configKey, value); - - }); - - // JSON-encode the value, and then base64 encode that to get the format - // that KSM would expect - String jsonString = objectMapper.writeValueAsString(configMap); - String base64EncodedJson = Base64.getEncoder().encodeToString( - jsonString.getBytes(StandardCharsets.UTF_8)); - - // Finally, try to parse the newly generated token as a KSM config. If this - // works, the config should be fully functional - KsmConfig.parseKsmConfig(base64EncodedJson); - - // Make a copy of the existing attributes, modifying just the value for - // KSM_CONFIGURATION_ATTRIBUTE - attributes = new HashMap<>(attributes); - attributes.put( - KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, base64EncodedJson); - - // Set the newly updated attributes back to the original object - entity.setAttributes(attributes); - - } - - // The KSM SDK only throws raw Exceptions, so we can't be more specific - catch (Exception exception) { - - // If the parsing attempt fails, throw a translatable error for display - // on the frontend - throw new TranslatableGuacamoleClientException( - "Invalid one-time KSM token provided for " - + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE + " attribute", - "CONNECTION_GROUP_ATTRIBUTES.ERROR_INVALID_KSM_ONE_TIME_TOKEN", - exception); - } - } - - } + @Inject + private KsmAttributeService attributeService; @Override public Directory getConnectionDirectory( @@ -259,40 +86,45 @@ public class KsmDirectoryService extends VaultDirectoryService { // validate KSM configurations, and translate one-time-tokens, if possible, // as well as ensuring that all ConnectionGroups returned include the // KSM_CONFIGURATION_ATTRIBUTE attribute, so it will be available in the UI. - return new DecoratingDirectory(underlyingDirectory) { + // The value of the KSM_CONFIGURATION_ATTRIBUTE will be sanitized if set. + return new KsmDirectory(underlyingDirectory) { @Override public void add(ConnectionGroup connectionGroup) throws GuacamoleException { - // Check for the KSM config attribute and translate the one-time token - // if possible before adding - processAttributes(connectionGroup); + // Process attribute values before saving + connectionGroup.setAttributes( + attributeService.processAttributes( + connectionGroup.getAttributes())); + super.add(connectionGroup); } @Override public void update(ConnectionGroup connectionGroup) throws GuacamoleException { - // Check for the KSM config attribute and translate the one-time token - // if possible before updating - processAttributes(connectionGroup); + // Unwrap the existing ConnectionGroup + if (connectionGroup instanceof KsmConnectionGroup) + connectionGroup = ((KsmConnectionGroup) connectionGroup).getUnderlyingConnectionGroup(); + + // Process attribute values before saving + connectionGroup.setAttributes( + attributeService.processAttributes( + connectionGroup.getAttributes())); + super.update(connectionGroup); - } - - @Override - protected ConnectionGroup decorate(ConnectionGroup connectionGroup) throws GuacamoleException { - - // Wrap the existing connection group in a KsmConnection to ensure the presence of the - // KSM_CONFIGURATION_ATTRIBUTE attribute - return new KsmConnectionGroup(connectionGroup); } @Override - protected ConnectionGroup undecorate(ConnectionGroup connectionGroup) throws GuacamoleException { + protected ConnectionGroup wrap(ConnectionGroup object) { - // Return the underlying connection group that the KsmConnectionGroup wraps - return ((KsmConnectionGroup) connectionGroup).getUnderlyConnectionGroup(); + // Do not process the ConnectionGroup further if it does not exist + if (object == null) + return null; + + // Sanitize values when a ConnectionGroup is fetched from the directory + return new KsmConnectionGroup(object); } @@ -307,40 +139,47 @@ public class KsmDirectoryService extends VaultDirectoryService { // validate KSM configurations, and translate one-time-tokens, if possible // Additionally, this directory will will decorate all users with a // KsmUser wrapper to ensure that all defined KSM fields will be exposed - // in the user attributes. - return new DecoratingDirectory(underlyingDirectory) { + // in the user attributes. The value of the KSM_CONFIGURATION_ATTRIBUTE + // will be sanitized if set. + return new KsmDirectory(underlyingDirectory) { @Override public void add(User user) throws GuacamoleException { - // Check for the KSM config attribute and translate the one-time token - // if possible before adding - processAttributes(user); + // Process attribute values before saving + user.setAttributes( + attributeService.processAttributes( + user.getAttributes())); + super.add(user); } @Override public void update(User user) throws GuacamoleException { - // Check for the KSM config attribute and translate the one-time token - // if possible before updating - processAttributes(user); + // Unwrap the existing user + if (user instanceof KsmUser) + user = ((KsmUser) user).getUnderlyingUser(); + + // Process attribute values before saving + user.setAttributes( + attributeService.processAttributes( + user.getAttributes())); + super.update(user); + } @Override - protected User decorate(User user) throws GuacamoleException { + protected User wrap(User object) { - // Wrap in a KsmUser class to ensure that all defined KSM fields will be - // present - return new KsmUser(user); - } + // Do not process the user further if it does not exist + if (object == null) + return null; - @Override - protected User undecorate(User user) throws GuacamoleException { + // Sanitize values when a user is fetched from the directory + return ksmUserFactory.create(object); - // Unwrap the KsmUser - return ((KsmUser) user).getUnderlyingUser(); } }; diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java index dc2877564..9c002eb4d 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUser.java @@ -21,27 +21,49 @@ package org.apache.guacamole.vault.ksm.user; import java.util.Map; -import org.apache.guacamole.net.auth.DelegatingUser; +import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.DelegatingUser; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; +import org.apache.guacamole.vault.ksm.conf.KsmConfigurationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.collect.Maps; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; -/** - * A User that explicitly adds a blank entry for any defined - * KSM user attributes. - */ -public class KsmUser extends DelegatingUser { + /** + * A KSM-specific user implementation that exposes the + * KSM_CONFIGURATION_ATTRIBUTE attribute even if no value is set. but only + * if user-specific KSM configuration is enabled. The value of the attribute + * will be sanitized if non-empty. This ensures that the attribute will always + * show up in the UI when the feature is enabled, even for users that don't + * already have it set, and that any sensitive information in the attribute + * value will not be exposed. + */ + public class KsmUser extends DelegatingUser { /** - * Create a new Vault user wrapping the provided User record. Any - * KSM-specific attribute forms will have empty values automatically - * populated when getAttributes() is called. + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(KsmUser.class); + + /** + * Service for retrieving KSM configuration details. + */ + @Inject + private KsmConfigurationService configurationService; + + /** + * Create a new Ksmuser wrapping the provided User record. * * @param user - * The user record to wrap. + * The User record to wrap. */ - KsmUser(User user) { + @AssistedInject + KsmUser(@Assisted User user) { super(user); } @@ -61,9 +83,33 @@ public class KsmUser extends DelegatingUser { // Make a copy of the existing map Map attributes = Maps.newHashMap(super.getAttributes()); - // Add the configuration attribute - attributes.putIfAbsent(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, null); + // Figure out if user-level KSM config is enabled + boolean userKsmConfigEnabled = false; + try { + userKsmConfigEnabled = configurationService.getAllowUserConfig(); + } catch (GuacamoleException e) { + + logger.warn( + "Disabling user KSM config due to exception: {}" + , e.getMessage()); + logger.debug("Error looking up if user KSM config is enabled.", e); + + } + + // If user-specific KSM configuration is not enabled, do not expose the + // attribute at all + if (!userKsmConfigEnabled) + attributes.remove(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE); + + else + // Sanitize the KSM configuration attribute, and ensure the attribute + // is always present + attributes.put( + KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE, + KsmAttributeService.sanitizeKsmAttributeValue( + attributes.get(KsmAttributeService.KSM_CONFIGURATION_ATTRIBUTE))); + return attributes; } -} + } \ No newline at end of file diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUserFactory.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUserFactory.java new file mode 100644 index 000000000..9aafbb622 --- /dev/null +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmUserFactory.java @@ -0,0 +1,40 @@ +/* + * 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.vault.ksm.user; + +import org.apache.guacamole.net.auth.User; + +/** + * Factory for creating KSM-specific users, which wrap an underlying User. + */ +public interface KsmUserFactory { + + /** + * Returns a new instance of a KsmUser, wrapping the provided underlying User. + * + * @param user + * The underlying User that should be wrapped. + * + * @return + * A new instance of a KsmUser, wrapping the provided underlying User. + */ + KsmUser create(User user); + +} From b8d2a9edc19ca495576b44802eb61e617824df32 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 29 Sep 2022 03:12:13 +0000 Subject: [PATCH 079/289] GUACAMOLE-1656: Check if user KSM config is null before constructing a client. --- .../org/apache/guacamole/vault/ksm/secret/KsmSecretService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index 515c1fc3d..f4d3c0626 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -163,7 +163,7 @@ public class KsmSecretService implements VaultSecretService { // If the user config happens to be the same as admin-defined one, // don't bother trying again - if (!Objects.equal(userKsmConfig, ksmConfig)) + if (userKsmConfig != null && !Objects.equal(userKsmConfig, ksmConfig)) return getClient(userKsmConfig).getSecret(name); return CompletableFuture.completedFuture(null); From e1794550ef317fc4cd6eb88050f9dfb6117f9aec Mon Sep 17 00:00:00 2001 From: Joshua Lancaster Date: Fri, 30 Sep 2022 07:21:25 -0700 Subject: [PATCH 080/289] GUACAMOLE-1628: Docker image of guacamole client is missing unzip --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5bafc1972..c482775e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,10 +53,10 @@ RUN /opt/guacamole/bin/build-guacamole.sh "$BUILD_DIR" /opt/guacamole "$BUILD_PR # For the runtime image, we start with the official Tomcat distribution FROM tomcat:${TOMCAT_VERSION}-${TOMCAT_JRE} -# Install XMLStarlet for server.xml alterations +# Install XMLStarlet for server.xml alterations and unzip for LOGBACK_LEVEL case RUN apt-get update -qq \ - && apt-get install -y xmlstarlet \ - && rm -rf /var/lib/apt/lists/* + && apt-get install -y xmlstarlet unzip\ + && rm -rf /var/lib/apt/lists/* # This is where the build artifacts go in the runtime image WORKDIR /opt/guacamole From 417587259f427db04624b731d7536ee048b3856a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 20 Sep 2022 17:07:17 -0700 Subject: [PATCH 081/289] GUACAMOLE-1224: Add CRUD-type events for Directory modifications. --- .../apache/guacamole/net/auth/Directory.java | 98 +++++++ .../guacamole/net/event/DirectoryEvent.java | 111 ++++++++ .../net/event/DirectoryFailureEvent.java | 36 +++ .../net/event/DirectorySuccessEvent.java | 35 +++ .../guacamole/net/auth/DirectoryTest.java | 124 +++++++++ .../ActiveConnectionDirectoryResource.java | 10 +- .../ActiveConnectionResource.java | 29 +-- .../ConnectionDirectoryResource.java | 10 +- .../rest/connection/ConnectionResource.java | 33 ++- .../ConnectionGroupDirectoryResource.java | 34 +-- .../ConnectionGroupResource.java | 27 +- .../directory/DirectoryObjectResource.java | 198 +++++++++++++- .../DirectoryObjectResourceFactory.java | 8 +- .../rest/directory/DirectoryResource.java | 244 +++++++++++++++++- .../directory/DirectoryResourceFactory.java | 7 +- .../rest/session/SessionResource.java | 2 +- .../rest/session/UserContextResource.java | 39 ++- .../session/UserContextResourceFactory.java | 7 +- .../SharingProfileDirectoryResource.java | 10 +- .../SharingProfileResource.java | 26 +- .../rest/tunnel/TunnelCollectionResource.java | 2 +- .../guacamole/rest/tunnel/TunnelResource.java | 15 +- .../rest/tunnel/TunnelResourceFactory.java | 6 +- .../rest/user/UserDirectoryResource.java | 9 +- .../guacamole/rest/user/UserResource.java | 45 ++-- .../usergroup/UserGroupDirectoryResource.java | 10 +- .../rest/usergroup/UserGroupResource.java | 23 +- 27 files changed, 1022 insertions(+), 176 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryFailureEvent.java create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectorySuccessEvent.java create mode 100644 guacamole-ext/src/test/java/org/apache/guacamole/net/auth/DirectoryTest.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java index 287145fc0..ba1e1522d 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java @@ -34,6 +34,104 @@ import org.apache.guacamole.GuacamoleException; */ public interface Directory { + /** + * All Directory types that may be found on the {@link UserContext} + * interface. + */ + public enum Type { + + /** + * The type of a Directory that contains {@link ActiveConnection} + * objects. + */ + ACTIVE_CONNECTION(ActiveConnection.class), + + /** + * The type of a Directory that contains {@link Connection} + * objects. + */ + CONNECTION(Connection.class), + + /** + * The type of a Directory that contains {@link ConnectionGroup} + * objects. + */ + CONNECTION_GROUP(ConnectionGroup.class), + + /** + * The type of a Directory that contains {@link SharingProfile} + * objects. + */ + SHARING_PROFILE(SharingProfile.class), + + /** + * The type of a Directory that contains {@link User} objects. + */ + USER(User.class), + + /** + * The type of a Directory that contains {@link UserGroup} + * objects. + */ + USER_GROUP(UserGroup.class); + + /** + * The base class of the type of object stored within the type of + * Directory represented by this Directory.Type. + */ + private final Class objectType; + + /** + * Creates a new Directory.Type representing the type of a Directory + * that contains only subclasses of the given class. + * + * @param objectType + * The base class of the type of object stored within the type of + * Directory represented by this Directory.Type. + */ + private Type(Class objectType) { + this.objectType = objectType; + } + + /** + * Returns the base class of the type of object stored within a + * {@link Directory} of this type. + * + * @return + * The base class of the type of object stored within a + * {@link Directory} of this type. + */ + public Class getObjectType() { + return objectType; + } + + /** + * Returns the Directory.Type representing the type of a Directory that + * could contain an object having the given class. The class may be a + * subclass of the overall base class of the objects stored within the + * Directory. + * + * @param objectType + * The class to determine the Directory.Type of. + * + * @return + * The Directory.Type representing the type of a Directory that + * could contain an object having the given class, or null if there + * is no such Directory available via the UserContext interface. + */ + public static Type of(Class objectType) { + + for (Type type : Type.values()) { + if (type.getObjectType().isAssignableFrom(objectType)) + return type; + } + + return null; + + } + + } + /** * Returns the object having the given identifier. Note that changes to * the object returned will not necessarily affect the object stored within diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java new file mode 100644 index 000000000..2ff74bf94 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java @@ -0,0 +1,111 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.Identifiable; + +/** + * Abstract basis for events which involve a modification made to the objects + * within a {@link Directory}. + * + * @param + * The type of object stored within the {@link Directory}. + */ +public interface DirectoryEvent + extends AuthenticationProviderEvent, UserEvent { + + /** + * The types of directory operations that may be represented by a + * DirectoryEvent. + */ + public enum Operation { + + /** + * An object was added to the {@link Directory}. The object added can + * be accessed with {@link #getObject()}, and its identifier may be + * obtained from {@link #getObjectIdentifier()}. + */ + ADD, + + /** + * An object was retrieved from a {@link Directory}. The object + * retrieved can be accessed with {@link #getObject()}, and its + * identifier may be obtained from {@link #getObjectIdentifier()}. + */ + GET, + + /** + * An existing object within a {@link Directory} was modified. The + * modified object can be accessed with {@link #getObject()}, and its + * identifier may be obtained from {@link #getObjectIdentifier()}. + */ + UPDATE, + + /** + * An existing object within a {@link Directory} was deleted/removed. + * The identifier of the object that was deleted may be obtained from + * {@link #getObjectIdentifier()}. The full object that was deleted + * will be made available via {@link #getObject()} if possible, but + * this is not guaranteed for deletions. + */ + REMOVE + + } + + /** + * Returns the type of objects stored within the {@link Directory} + * affected by the operation. + * + * @return + * The type of objects stored within the {@link Directory}. + */ + Directory.Type getDirectoryType(); + + /** + * Returns the operation that was performed/attempted. + * + * @return + * The operation that was performed or attempted. + */ + Operation getOperation(); + + /** + * Returns the identifier of the object affected by the operation. If the + * object was just created, this will be the identifier of the new object. + * + * @return + * The identifier of the object affected by the operation. + */ + String getObjectIdentifier(); + + /** + * Returns the object affected by the operation, if available. For + * deletions, there is no guarantee that the affected object will be + * available within this event. If the object is not available, null is + * returned. + * + * @return + * The object affected by the operation performed, or null if that + * object is not available in the context of this event. + */ + ObjectType getObject(); + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryFailureEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryFailureEvent.java new file mode 100644 index 000000000..a9ec474d1 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryFailureEvent.java @@ -0,0 +1,36 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.Identifiable; + +/** + * Event that is dispatched whenever a REST API request to create/modify/delete + * an object within a {@link Directory} fails. The specific failure is made + * available via {@link #getFailure()}. + * + * @param + * The type of object stored within the {@link Directory}. + */ +public interface DirectoryFailureEvent + extends DirectoryEvent, FailureEvent { + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectorySuccessEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectorySuccessEvent.java new file mode 100644 index 000000000..691cee7f7 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectorySuccessEvent.java @@ -0,0 +1,35 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.Identifiable; + +/** + * Event that is dispatched whenever a REST API request to create/modify/delete + * an object within a {@link Directory} succeeds. + * + * @param + * The type of object stored within the {@link Directory}. + */ +public interface DirectorySuccessEvent + extends DirectoryEvent { + +} diff --git a/guacamole-ext/src/test/java/org/apache/guacamole/net/auth/DirectoryTest.java b/guacamole-ext/src/test/java/org/apache/guacamole/net/auth/DirectoryTest.java new file mode 100644 index 000000000..04623aa58 --- /dev/null +++ b/guacamole-ext/src/test/java/org/apache/guacamole/net/auth/DirectoryTest.java @@ -0,0 +1,124 @@ +/* + * 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.auth; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test that verifies the functionality provided by the Directory interface. + */ +public class DirectoryTest { + + /** + * Returns a Collection of all classes that have associated Directories + * available via the UserContext interface. The classes are retrieved + * using reflection by enumerating the type parameters of the return types + * of all functions that return a Directory. + * + * @return + * A Collection of all classes that have associated Directories + * available via the UserContext interface. + */ + @SuppressWarnings("unchecked") // Verified via calls to isAssignableFrom() + private Collection> getDirectoryTypes() { + + Set> types = new HashSet<>(); + + Method[] methods = UserContext.class.getMethods(); + for (Method method : methods) { + + if (!Directory.class.isAssignableFrom(method.getReturnType())) + continue; + + Type retType = method.getGenericReturnType(); + Assert.assertTrue("UserContext functions that return directories " + + "must have proper type parameters for the returned " + + "directory.", retType instanceof ParameterizedType); + + Type[] typeArgs = ((ParameterizedType) retType).getActualTypeArguments(); + Assert.assertEquals("UserContext functions that return directories " + + "must properly declare exactly one type argument for " + + "those directories.", 1, typeArgs.length); + + Class directoryType = (Class) typeArgs[0]; + Assert.assertTrue("Directories returned by UserContext functions " + + "must contain subclasses of Identifiable.", + Identifiable.class.isAssignableFrom(directoryType)); + + types.add((Class) directoryType); + + } + + return Collections.unmodifiableSet(types); + + } + + /** + * Verifies that Directory.Type covers the types of all directories exposed + * by the UserContext interface. + */ + @Test + public void testTypeCoverage() { + + Collection> types = getDirectoryTypes(); + + Assert.assertEquals("Directory.Type must provide exactly one value " + + "for each type of directory provideed by the UserContext " + + "interface.", types.size(), Directory.Type.values().length); + + for (Class type : types) { + + Directory.Type dirType = Directory.Type.of(type); + Assert.assertNotNull("of() must provide mappings for all directory " + + "types defined on the UserContext interface.", dirType); + + Assert.assertEquals("getObjectType() must return the same base " + + "superclass used by UserContext for all directory " + + "types defined on the UserContext interface.", type, + dirType.getObjectType()); + + } + + } + + /** + * Verifies that each type declared by Directory.Type exposes an + * associated class via getObjectType() which then maps back to the same + * type via Directory.Type.of(). + */ + @Test + public void testTypeIdentity() { + for (Directory.Type dirType : Directory.Type.values()) { + Assert.assertEquals("For all defined directory types, " + + "Directory.Type.of(theType.getObjectType()) must " + + "correctly map back to theType.", dirType, + Directory.Type.of(dirType.getObjectType())); + } + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionDirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionDirectoryResource.java index 5296565ab..88319f01c 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionDirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionDirectoryResource.java @@ -26,6 +26,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.ActiveConnection; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Permissions; import org.apache.guacamole.net.auth.UserContext; @@ -48,6 +49,9 @@ public class ActiveConnectionDirectoryResource * operations and subresources available for the given ActiveConnection * Directory. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -63,11 +67,13 @@ public class ActiveConnectionDirectoryResource * representing ActiveConnections. */ @AssistedInject - public ActiveConnectionDirectoryResource(@Assisted UserContext userContext, + public ActiveConnectionDirectoryResource( + @Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, DirectoryObjectTranslator translator, DirectoryObjectResourceFactory resourceFactory) { - super(userContext, directory, translator, resourceFactory); + super(authenticatedUser, userContext, ActiveConnection.class, directory, translator, resourceFactory); } @Override diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionResource.java index 514daa124..59afe18d1 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/ActiveConnectionResource.java @@ -30,6 +30,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.ActiveConnection; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.UserContext; @@ -47,17 +48,6 @@ import org.apache.guacamole.rest.directory.DirectoryResourceFactory; public class ActiveConnectionResource extends DirectoryObjectResource { - /** - * The UserContext associated with the Directory which contains the - * Connection exposed by this resource. - */ - private final UserContext userContext; - - /** - * The ActiveConnection exposed by this ActiveConnectionResource. - */ - private final ActiveConnection activeConnection; - /** * A factory which can be used to create instances of resources representing * Connection. @@ -70,6 +60,9 @@ public class ActiveConnectionResource * Creates a new ActiveConnectionResource which exposes the operations and * subresources available for the given ActiveConnection. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -85,13 +78,12 @@ public class ActiveConnectionResource * ActiveConnections. */ @AssistedInject - public ActiveConnectionResource(@Assisted UserContext userContext, + public ActiveConnectionResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, @Assisted ActiveConnection activeConnection, DirectoryObjectTranslator translator) { - super(userContext, directory, activeConnection, translator); - this.userContext = userContext; - this.activeConnection = activeConnection; + super(authenticatedUser, userContext, ActiveConnection.class, directory, activeConnection, translator); } /** @@ -109,9 +101,12 @@ public class ActiveConnectionResource public DirectoryObjectResource getConnection() throws GuacamoleException { + UserContext userContext = getUserContext(); + ActiveConnection activeConnection = getInternalObject(); + // Return the underlying connection as a resource return connectionDirectoryResourceFactory - .create(userContext, userContext.getConnectionDirectory()) + .create(getAuthenticatedUser(), userContext, userContext.getConnectionDirectory()) .getObjectResource(activeConnection.getConnectionIdentifier()); } @@ -137,7 +132,7 @@ public class ActiveConnectionResource throws GuacamoleException { // Generate and return sharing credentials for the active connection - return new APIUserCredentials(activeConnection.getSharingCredentials(sharingProfileIdentifier)); + return new APIUserCredentials(getInternalObject().getSharingCredentials(sharingProfileIdentifier)); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionDirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionDirectoryResource.java index 88408a796..612e05502 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionDirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionDirectoryResource.java @@ -25,6 +25,7 @@ import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Permissions; @@ -47,6 +48,9 @@ public class ConnectionDirectoryResource * Creates a new ConnectionDirectoryResource which exposes the operations * and subresources available for the given Connection Directory. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -62,11 +66,11 @@ public class ConnectionDirectoryResource * representing Connections. */ @AssistedInject - public ConnectionDirectoryResource(@Assisted UserContext userContext, - @Assisted Directory directory, + public ConnectionDirectoryResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, DirectoryObjectTranslator translator, DirectoryObjectResourceFactory resourceFactory) { - super(userContext, directory, translator, resourceFactory); + super(authenticatedUser, userContext, Connection.class, directory, translator, resourceFactory); } @Override diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java index 15586e187..484543ba2 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/connection/ConnectionResource.java @@ -32,6 +32,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleUnsupportedException; import org.apache.guacamole.net.auth.ActivityRecordSet; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Permissions; @@ -65,17 +66,6 @@ public class ConnectionResource extends DirectoryObjectResource directory, @Assisted Connection connection, DirectoryObjectTranslator translator) { - super(userContext, directory, connection, translator); - this.userContext = userContext; - this.connection = connection; + super(authenticatedUser, userContext, Connection.class, directory, connection, translator); } /** @@ -126,8 +118,10 @@ public class ConnectionResource extends DirectoryObjectResource getConnectionParameters() throws GuacamoleException { + Connection connection = getInternalObject(); + // Pull effective permissions - Permissions effective = userContext.self().getEffectivePermissions(); + Permissions effective = getUserContext().self().getEffectivePermissions(); // Retrieve permission sets SystemPermissionSet systemPermissions = effective.getSystemPermissions(); @@ -162,6 +156,8 @@ public class ConnectionResource extends DirectoryObjectResource getSharingProfileDirectoryResource() throws GuacamoleException { + UserContext userContext = getUserContext(); + Connection connection = getInternalObject(); + // Produce subset of all SharingProfiles, containing only those which // are associated with this connection Directory sharingProfiles = new DirectoryView<>( @@ -209,7 +208,7 @@ public class ConnectionResource extends DirectoryObjectResource { - /** - * The UserContext associated with the Directory which contains the - * ConnectionGroup exposed by this resource. - */ - private final UserContext userContext; - - /** - * The Directory exposed by this resource. - */ - private final Directory directory; - - /** - * A factory which can be used to create instances of resources representing - * ConnectionGroups. - */ - private final DirectoryObjectResourceFactory resourceFactory; - /** * Creates a new ConnectionGroupDirectoryResource which exposes the * operations and subresources available for the given ConnectionGroup * Directory. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -81,23 +68,24 @@ public class ConnectionGroupDirectoryResource * representing ConnectionGroups. */ @AssistedInject - public ConnectionGroupDirectoryResource(@Assisted UserContext userContext, + public ConnectionGroupDirectoryResource( + @Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, DirectoryObjectTranslator translator, DirectoryObjectResourceFactory resourceFactory) { - super(userContext, directory, translator, resourceFactory); - this.userContext = userContext; - this.directory = directory; - this.resourceFactory = resourceFactory; + super(authenticatedUser, userContext, ConnectionGroup.class, directory, translator, resourceFactory); } @Override public DirectoryObjectResource getObjectResource(String identifier) throws GuacamoleException { + UserContext userContext = getUserContext(); + // Use root group if identifier is the standard root identifier if (identifier != null && identifier.equals(APIConnectionGroup.ROOT_IDENTIFIER)) - return resourceFactory.create(userContext, directory, + return getResourceFactory().create(getAuthenticatedUser(), userContext, getDirectory(), userContext.getRootConnectionGroup()); return super.getObjectResource(identifier); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupResource.java index f91b8ebb9..22f41b0cf 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/ConnectionGroupResource.java @@ -29,6 +29,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.UserContext; @@ -45,21 +46,13 @@ import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; public class ConnectionGroupResource extends DirectoryObjectResource { - /** - * The UserContext associated with the Directory which contains the - * ConnectionGroup exposed by this resource. - */ - private final UserContext userContext; - - /** - * The ConnectionGroup object represented by this ConnectionGroupResource. - */ - private final ConnectionGroup connectionGroup; - /** * Creates a new ConnectionGroupResource which exposes the operations and * subresources available for the given ConnectionGroup. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -75,13 +68,13 @@ public class ConnectionGroupResource * object given. */ @AssistedInject - public ConnectionGroupResource(@Assisted UserContext userContext, + public ConnectionGroupResource( + @Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, @Assisted ConnectionGroup connectionGroup, DirectoryObjectTranslator translator) { - super(userContext, directory, connectionGroup, translator); - this.userContext = userContext; - this.connectionGroup = connectionGroup; + super(authenticatedUser, userContext, ConnectionGroup.class, directory, connectionGroup, translator); } /** @@ -107,8 +100,8 @@ public class ConnectionGroupResource throws GuacamoleException { // Retrieve the requested tree, filtering by the given permissions - ConnectionGroupTree tree = new ConnectionGroupTree(userContext, - connectionGroup, permissions); + ConnectionGroupTree tree = new ConnectionGroupTree(getUserContext(), + getInternalObject(), permissions); // Return tree as a connection group return tree.getRootAPIConnectionGroup(); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectResource.java index 7b354a226..8ed36fec0 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectResource.java @@ -19,6 +19,7 @@ package org.apache.guacamole.rest.directory; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -27,9 +28,15 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Identifiable; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.event.DirectoryEvent; +import org.apache.guacamole.net.event.DirectoryFailureEvent; +import org.apache.guacamole.net.event.DirectorySuccessEvent; +import org.apache.guacamole.rest.event.ListenerService; /** * A REST resource which abstracts the operations available on an existing @@ -51,12 +58,22 @@ import org.apache.guacamole.net.auth.UserContext; @Consumes(MediaType.APPLICATION_JSON) public abstract class DirectoryObjectResource { + /** + * The user that is accessing this resource. + */ + private final AuthenticatedUser authenticatedUser; + /** * The UserContext associated with the Directory containing the object * represented by this DirectoryObjectResource. */ private final UserContext userContext; + /** + * The type of object represented by this DirectoryObjectResource. + */ + private final Class internalType; + /** * The Directory which contains the object represented by this * DirectoryObjectResource. @@ -74,13 +91,26 @@ public abstract class DirectoryObjectResource translator; + /** + * Service for dispatching events to registered listeners. + */ + @Inject + private ListenerService listenerService; + /** * Creates a new DirectoryObjectResource which exposes the operations * available for the given object. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * + * @param internalType + * The type of object that this DirectoryObjectResource should + * represent. + * * @param directory * The Directory which contains the given object. * @@ -91,15 +121,162 @@ public abstract class DirectoryObjectResource internalType, Directory directory, InternalType object, DirectoryObjectTranslator translator) { + this.authenticatedUser = authenticatedUser; this.userContext = userContext; this.directory = directory; + this.internalType = internalType; this.object = object; this.translator = translator; } + /** + * Notifies all registered listeners that the given operation has succeeded + * against the object represented by this resource. + * + * @param operation + * The operation that was performed. + * + * @throws GuacamoleException + * If a listener throws a GuacamoleException from its event handler. + */ + protected void fireDirectorySuccessEvent(DirectoryEvent.Operation operation) + throws GuacamoleException { + listenerService.handleEvent(new DirectorySuccessEvent() { + + @Override + public Directory.Type getDirectoryType() { + return Directory.Type.of(internalType); + } + + @Override + public DirectoryEvent.Operation getOperation() { + return operation; + } + + @Override + public String getObjectIdentifier() { + return object.getIdentifier(); + } + + @Override + public InternalType getObject() { + return object; + } + + @Override + public AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + @Override + public AuthenticationProvider getAuthenticationProvider() { + return userContext.getAuthenticationProvider(); + } + + }); + } + + /** + * Notifies all registered listeners that the given operation has failed + * against the object represented by this resource. + * + * @param operation + * The operation that failed. + * + * @param failure + * The failure that occurred. + * + * @throws GuacamoleException + * If a listener throws a GuacamoleException from its event handler. + */ + protected void fireDirectoryFailureEvent(DirectoryEvent.Operation operation, + Throwable failure) throws GuacamoleException { + listenerService.handleEvent(new DirectoryFailureEvent() { + + @Override + public Directory.Type getDirectoryType() { + return Directory.Type.of(internalType); + } + + @Override + public DirectoryEvent.Operation getOperation() { + return operation; + } + + @Override + public String getObjectIdentifier() { + return object.getIdentifier(); + } + + @Override + public InternalType getObject() { + return object; + } + + @Override + public AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + @Override + public AuthenticationProvider getAuthenticationProvider() { + return userContext.getAuthenticationProvider(); + } + + @Override + public Throwable getFailure() { + return failure; + } + + }); + } + + /** + * Returns the user accessing this resource. + * + * @return + * The user accessing this resource. + */ + protected AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + /** + * Returns the UserContext providing the Directory that contains the object + * exposed by this resource. + * + * @return + * The UserContext providing the Directory that contains the object + * exposed by this resource. + */ + protected UserContext getUserContext() { + return userContext; + } + + /** + * Returns the Directory containing the object exposed by this resource. + * + * @return + * The Directory containing the object exposed by this resource. + */ + protected Directory getDirectory() { + return directory; + } + + /** + * Returns the object exposed by this resource. + * + * @return + * The object exposed by this resource. + */ + protected InternalType getInternalObject() { + return object; + } + /** * Returns the object represented by this DirectoryObjectResource, in a * format intended for interchange. @@ -138,7 +315,15 @@ public abstract class DirectoryObjectResource - create(UserContext userContext, Directory directory, - InternalType object); + create(AuthenticatedUser authenticatedUser, UserContext userContext, + Directory directory, InternalType object); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index e9b4b225b..50d428528 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PATCH; @@ -36,6 +37,8 @@ import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleUnsupportedException; +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Identifiable; import org.apache.guacamole.net.auth.Permissions; @@ -44,7 +47,11 @@ import org.apache.guacamole.net.auth.permission.ObjectPermission; import org.apache.guacamole.net.auth.permission.ObjectPermissionSet; import org.apache.guacamole.net.auth.permission.SystemPermission; import org.apache.guacamole.net.auth.permission.SystemPermissionSet; +import org.apache.guacamole.net.event.DirectoryEvent; +import org.apache.guacamole.net.event.DirectoryFailureEvent; +import org.apache.guacamole.net.event.DirectorySuccessEvent; import org.apache.guacamole.rest.APIPatch; +import org.apache.guacamole.rest.event.ListenerService; /** * A REST resource which abstracts the operations available on all Guacamole @@ -68,12 +75,23 @@ import org.apache.guacamole.rest.APIPatch; @Consumes(MediaType.APPLICATION_JSON) public abstract class DirectoryResource { + /** + * The user that is accessing this resource. + */ + private final AuthenticatedUser authenticatedUser; + /** * The UserContext associated with the Directory being exposed by this * DirectoryResource. */ private final UserContext userContext; + /** + * The type of object contained within the Directory exposed by this + * DirectoryResource. + */ + private final Class internalType; + /** * The Directory being exposed by this DirectoryResource. */ @@ -92,13 +110,25 @@ public abstract class DirectoryResource resourceFactory; + /** + * Service for dispatching events to registered listeners. + */ + @Inject + private ListenerService listenerService; + /** * Creates a new DirectoryResource which exposes the operations available * for the given Directory. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * + * @param internalType + * The type of object contained within the given Directory. + * * @param directory * The Directory being exposed by this DirectoryResource. * @@ -110,11 +140,15 @@ public abstract class DirectoryResource directory, + public DirectoryResource(AuthenticatedUser authenticatedUser, + UserContext userContext, Class internalType, + Directory directory, DirectoryObjectTranslator translator, DirectoryObjectResourceFactory resourceFactory) { + this.authenticatedUser = authenticatedUser; this.userContext = userContext; this.directory = directory; + this.internalType = internalType; this.translator = translator; this.resourceFactory = resourceFactory; } @@ -139,6 +173,174 @@ public abstract class DirectoryResource() { + + @Override + public Directory.Type getDirectoryType() { + return Directory.Type.of(internalType); + } + + @Override + public DirectoryEvent.Operation getOperation() { + return operation; + } + + @Override + public String getObjectIdentifier() { + return identifier; + } + + @Override + public InternalType getObject() { + return object; + } + + @Override + public AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + @Override + public AuthenticationProvider getAuthenticationProvider() { + return userContext.getAuthenticationProvider(); + } + + }); + } + + /** + * Notifies all registered listeners that the given operation has failed + * against the object having the given identifier within the Directory + * represented by this resource. + * + * @param operation + * The operation that failed. + * + * @param identifier + * The identifier of the object that would have been affected by the + * operation had it succeeded. + * + * @param object + * The specific object would have been affected by the operation, if + * available, had it succeeded, including any changes that were + * intended to be applied to the object. If not available, this may be + * null. + * + * @param failure + * The failure that occurred. + * + * @throws GuacamoleException + * If a listener throws a GuacamoleException from its event handler. + */ + protected void fireDirectoryFailureEvent(DirectoryEvent.Operation operation, + String identifier, InternalType object, Throwable failure) + throws GuacamoleException { + listenerService.handleEvent(new DirectoryFailureEvent() { + + @Override + public Directory.Type getDirectoryType() { + return Directory.Type.of(internalType); + } + + @Override + public DirectoryEvent.Operation getOperation() { + return operation; + } + + @Override + public String getObjectIdentifier() { + return identifier; + } + + @Override + public InternalType getObject() { + return object; + } + + @Override + public AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + @Override + public AuthenticationProvider getAuthenticationProvider() { + return userContext.getAuthenticationProvider(); + } + + @Override + public Throwable getFailure() { + return failure; + } + + }); + } + + /** + * Returns the user accessing this resource. + * + * @return + * The user accessing this resource. + */ + protected AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + /** + * Returns the UserContext providing the Directory exposed by this + * resource. + * + * @return + * The UserContext providing the Directory exposed by this resource. + */ + protected UserContext getUserContext() { + return userContext; + } + + /** + * Returns the Directory exposed by this resource. + * + * @return + * The Directory exposed by this resource. + */ + protected Directory getDirectory() { + return directory; + } + + /** + * Returns a factory that can be used to create instances of resources + * representing individual objects contained within the Directory exposed + * by this resource. + * + * @return + * A factory that can be used to create instances of resources + * representing individual objects contained within the Directory + * exposed by this resource. + */ + public DirectoryObjectResourceFactory getResourceFactory() { + return resourceFactory; + } + /** * Returns a map of all objects available within this DirectoryResource, * filtering the returned map by the given permission, if specified. @@ -213,7 +415,15 @@ public abstract class DirectoryResource resource = resourceFactory.create(authenticatedUser, userContext, directory, object); + fireDirectorySuccessEvent(DirectoryEvent.Operation.GET, identifier, object); + return resource; } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResourceFactory.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResourceFactory.java index 357777a6f..c28294bae 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResourceFactory.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResourceFactory.java @@ -19,6 +19,7 @@ package org.apache.guacamole.rest.directory; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Identifiable; import org.apache.guacamole.net.auth.UserContext; @@ -41,6 +42,9 @@ public interface DirectoryResourceFactory - create(UserContext userContext, Directory directory); + create(AuthenticatedUser authenticatedUser, UserContext userContext, + Directory directory); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java index 0c334b068..6116c3785 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/session/SessionResource.java @@ -116,7 +116,7 @@ public class SessionResource { UserContext userContext = session.getUserContext(authProviderIdentifier); // Return a resource exposing the retrieved UserContext - return userContextResourceFactory.create(userContext); + return userContextResourceFactory.create(session.getAuthenticatedUser(), userContext); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java index cbb04d172..b696259c3 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java @@ -30,6 +30,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.ActiveConnection; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.SharingProfile; @@ -59,6 +60,11 @@ public class UserContextResource { */ private final UserContext userContext; + /** + * The user that is accessing this resource. + */ + private final AuthenticatedUser authenticatedUser; + /** * Factory for creating DirectoryObjectResources which expose a given User. */ @@ -115,12 +121,17 @@ public class UserContextResource { * Creates a new UserContextResource which exposes the data within the * given UserContext. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext which should be exposed through this * UserContextResource. */ @AssistedInject - public UserContextResource(@Assisted UserContext userContext) { + public UserContextResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext) { + this.authenticatedUser = authenticatedUser; this.userContext = userContext; } @@ -140,7 +151,7 @@ public class UserContextResource { @Path("self") public DirectoryObjectResource getSelfResource() throws GuacamoleException { - return userResourceFactory.create(userContext, + return userResourceFactory.create(authenticatedUser, userContext, userContext.getUserDirectory(), userContext.self()); } @@ -158,8 +169,8 @@ public class UserContextResource { @Path("activeConnections") public DirectoryResource getActiveConnectionDirectoryResource() throws GuacamoleException { - return activeConnectionDirectoryResourceFactory.create(userContext, - userContext.getActiveConnectionDirectory()); + return activeConnectionDirectoryResourceFactory.create(authenticatedUser, + userContext, userContext.getActiveConnectionDirectory()); } /** @@ -176,8 +187,8 @@ public class UserContextResource { @Path("connections") public DirectoryResource getConnectionDirectoryResource() throws GuacamoleException { - return connectionDirectoryResourceFactory.create(userContext, - userContext.getConnectionDirectory()); + return connectionDirectoryResourceFactory.create(authenticatedUser, + userContext, userContext.getConnectionDirectory()); } /** @@ -194,8 +205,8 @@ public class UserContextResource { @Path("connectionGroups") public DirectoryResource getConnectionGroupDirectoryResource() throws GuacamoleException { - return connectionGroupDirectoryResourceFactory.create(userContext, - userContext.getConnectionGroupDirectory()); + return connectionGroupDirectoryResourceFactory.create(authenticatedUser, + userContext, userContext.getConnectionGroupDirectory()); } /** @@ -212,8 +223,8 @@ public class UserContextResource { @Path("sharingProfiles") public DirectoryResource getSharingProfileDirectoryResource() throws GuacamoleException { - return sharingProfileDirectoryResourceFactory.create(userContext, - userContext.getSharingProfileDirectory()); + return sharingProfileDirectoryResourceFactory.create(authenticatedUser, + userContext, userContext.getSharingProfileDirectory()); } /** @@ -230,8 +241,8 @@ public class UserContextResource { @Path("users") public DirectoryResource getUserDirectoryResource() throws GuacamoleException { - return userDirectoryResourceFactory.create(userContext, - userContext.getUserDirectory()); + return userDirectoryResourceFactory.create(authenticatedUser, + userContext, userContext.getUserDirectory()); } /** @@ -248,8 +259,8 @@ public class UserContextResource { @Path("userGroups") public DirectoryResource getUserGroupDirectoryResource() throws GuacamoleException { - return userGroupDirectoryResourceFactory.create(userContext, - userContext.getUserGroupDirectory()); + return userGroupDirectoryResourceFactory.create(authenticatedUser, + userContext, userContext.getUserGroupDirectory()); } /** diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResourceFactory.java b/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResourceFactory.java index 5eea3edf5..c969a6311 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResourceFactory.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResourceFactory.java @@ -19,6 +19,7 @@ package org.apache.guacamole.rest.session; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.UserContext; /** @@ -31,6 +32,9 @@ public interface UserContextResourceFactory { * Creates a new UserContextResource which exposes the contents of the * given UserContext. * + * @param authenticatedUser + * The user that is accessing the resource. + * * @param userContext * The UserContext whose contents should be exposed. * @@ -38,6 +42,7 @@ public interface UserContextResourceFactory { * A new UserContextResource which exposes the contents of the given * UserContext. */ - UserContextResource create(UserContext userContext); + UserContextResource create(AuthenticatedUser authenticatedUser, + UserContext userContext); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileDirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileDirectoryResource.java index ab24ef381..16ae29509 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileDirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileDirectoryResource.java @@ -25,6 +25,7 @@ import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Permissions; import org.apache.guacamole.net.auth.SharingProfile; @@ -48,6 +49,9 @@ public class SharingProfileDirectoryResource * operations and subresources available for the given SharingProfile * Directory. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -63,11 +67,13 @@ public class SharingProfileDirectoryResource * representing SharingProfiles. */ @AssistedInject - public SharingProfileDirectoryResource(@Assisted UserContext userContext, + public SharingProfileDirectoryResource( + @Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, DirectoryObjectTranslator translator, DirectoryObjectResourceFactory resourceFactory) { - super(userContext, directory, translator, resourceFactory); + super(authenticatedUser, userContext, SharingProfile.class, directory, translator, resourceFactory); } @Override diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileResource.java index 4797ade7f..46daac9b3 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/sharingprofile/SharingProfileResource.java @@ -29,6 +29,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Permissions; import org.apache.guacamole.net.auth.SharingProfile; @@ -49,21 +50,13 @@ import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; public class SharingProfileResource extends DirectoryObjectResource { - /** - * The UserContext associated with the Directory which contains the - * SharingProfile exposed by this resource. - */ - private final UserContext userContext; - - /** - * The SharingProfile object represented by this SharingProfileResource. - */ - private final SharingProfile sharingProfile; - /** * Creates a new SharingProfileResource which exposes the operations and * subresources available for the given SharingProfile. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -78,13 +71,12 @@ public class SharingProfileResource * object given. */ @AssistedInject - public SharingProfileResource(@Assisted UserContext userContext, + public SharingProfileResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, @Assisted SharingProfile sharingProfile, DirectoryObjectTranslator translator) { - super(userContext, directory, sharingProfile, translator); - this.userContext = userContext; - this.sharingProfile = sharingProfile; + super(authenticatedUser, userContext, SharingProfile.class, directory, sharingProfile, translator); } /** @@ -103,8 +95,10 @@ public class SharingProfileResource public Map getParameters() throws GuacamoleException { + SharingProfile sharingProfile = getInternalObject(); + // Pull effective permissions - Permissions effective = userContext.self().getEffectivePermissions(); + Permissions effective = getUserContext().self().getEffectivePermissions(); // Retrieve permission sets SystemPermissionSet systemPermissions = effective.getSystemPermissions(); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelCollectionResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelCollectionResource.java index abd7b422d..770469c7e 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelCollectionResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelCollectionResource.java @@ -105,7 +105,7 @@ public class TunnelCollectionResource { throw new GuacamoleResourceNotFoundException("No such tunnel."); // Return corresponding tunnel resource - return tunnelResourceFactory.create(tunnel); + return tunnelResourceFactory.create(session.getAuthenticatedUser(), tunnel); } 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 7c0ec7eef..7107ab277 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 @@ -34,6 +34,7 @@ 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.AuthenticatedUser; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.protocols.ProtocolInfo; import org.apache.guacamole.rest.activeconnection.APIActiveConnection; @@ -55,6 +56,11 @@ public class TunnelResource { */ private static final String DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_OCTET_STREAM; + /** + * The user that is accessing this resource. + */ + private final AuthenticatedUser authenticatedUser; + /** * The tunnel that this TunnelResource represents. */ @@ -78,11 +84,16 @@ public class TunnelResource { * Creates a new TunnelResource which exposes the operations and * subresources available for the given tunnel. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param tunnel * The tunnel that this TunnelResource should represent. */ @AssistedInject - public TunnelResource(@Assisted UserTunnel tunnel) { + public TunnelResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserTunnel tunnel) { + this.authenticatedUser = authenticatedUser; this.tunnel = tunnel; } @@ -110,7 +121,7 @@ public class TunnelResource { throw new GuacamoleResourceNotFoundException("No readable active connection for tunnel."); // Return the associated ActiveConnection as a resource - return activeConnectionResourceFactory.create(userContext, + return activeConnectionResourceFactory.create(authenticatedUser, userContext, userContext.getActiveConnectionDirectory(), activeConnection); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResourceFactory.java b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResourceFactory.java index 2deacf88a..9efdf9c68 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResourceFactory.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/tunnel/TunnelResourceFactory.java @@ -19,6 +19,7 @@ package org.apache.guacamole.rest.tunnel; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.tunnel.UserTunnel; /** @@ -31,12 +32,15 @@ public interface TunnelResourceFactory { * Creates a new TunnelResource which exposes the contents of the * given tunnel. * + * @param authenticatedUser + * The user that is accessing the resource. + * * @param tunnel * The tunnel whose contents should be exposed. * * @return * A new TunnelResource which exposes the contents of the given tunnel. */ - TunnelResource create(UserTunnel tunnel); + TunnelResource create(AuthenticatedUser authenticatedUser, UserTunnel tunnel); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserDirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserDirectoryResource.java index f93016fe9..0918ca82f 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserDirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserDirectoryResource.java @@ -25,6 +25,7 @@ import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Permissions; @@ -46,6 +47,9 @@ public class UserDirectoryResource extends DirectoryResource { * Creates a new UserDirectoryResource which exposes the operations and * subresources available for the given User Directory. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -61,11 +65,12 @@ public class UserDirectoryResource extends DirectoryResource { * representing Users. */ @AssistedInject - public UserDirectoryResource(@Assisted UserContext userContext, + public UserDirectoryResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, DirectoryObjectTranslator translator, DirectoryObjectResourceFactory resourceFactory) { - super(userContext, directory, translator, resourceFactory); + super(authenticatedUser, userContext, User.class, directory, translator, resourceFactory); } @Override diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java index aa8a3ec7e..9b79fbb48 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java @@ -33,6 +33,7 @@ import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleUnsupportedException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.User; @@ -63,27 +64,13 @@ public class UserResource */ private static final Logger logger = LoggerFactory.getLogger(UserResource.class); - /** - * The UserContext associated with the Directory which contains the User - * exposed by this resource. - */ - private final UserContext userContext; - - /** - * The Directory which contains the User object represented by this - * UserResource. - */ - private final Directory directory; - - /** - * The User object represented by this UserResource. - */ - private final User user; - /** * Creates a new UserResource which exposes the operations and subresources * available for the given User. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -97,14 +84,12 @@ public class UserResource * A DirectoryObjectTranslator implementation which handles Users. */ @AssistedInject - public UserResource(@Assisted UserContext userContext, + public UserResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, @Assisted User user, DirectoryObjectTranslator translator) { - super(userContext, directory, user, translator); - this.userContext = userContext; - this.directory = directory; - this.user = user; + super(authenticatedUser, userContext, User.class, directory, user, translator); } /** @@ -122,6 +107,8 @@ public class UserResource public UserHistoryResource getUserHistory() throws GuacamoleException { + User user = getInternalObject(); + // First try to retrieve history using the current getUserHistory() method. try { return new UserHistoryResource(user.getUserHistory()); @@ -147,7 +134,7 @@ public class UserResource public void updateObject(APIUser modifiedObject) throws GuacamoleException { // A user may not use this endpoint to update their password - User currentUser = userContext.self(); + User currentUser = getUserContext().self(); if ( currentUser.getIdentifier().equals(modifiedObject.getUsername()) && modifiedObject.getPassword() != null) { @@ -178,13 +165,15 @@ public class UserResource public void updatePassword(APIUserPasswordUpdate userPasswordUpdate, @Context HttpServletRequest request) throws GuacamoleException { + User user = getInternalObject(); + // Build credentials Credentials credentials = new Credentials(user.getIdentifier(), userPasswordUpdate.getOldPassword(), request); // Verify that the old password was correct try { - AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); + AuthenticationProvider authProvider = getUserContext().getAuthenticationProvider(); if (authProvider.authenticateUser(credentials) == null) throw new GuacamoleSecurityException("Permission denied."); } @@ -196,7 +185,7 @@ public class UserResource // Set password to the newly provided one user.setPassword(userPasswordUpdate.getNewPassword()); - directory.update(user); + getDirectory().update(user); } @@ -211,7 +200,7 @@ public class UserResource */ @Path("permissions") public PermissionSetResource getPermissions() { - return new PermissionSetResource(user); + return new PermissionSetResource(getInternalObject()); } /** @@ -228,7 +217,7 @@ public class UserResource @GET @Path("effectivePermissions") public APIPermissionSet getEffectivePermissions() throws GuacamoleException { - return new APIPermissionSet(user.getEffectivePermissions()); + return new APIPermissionSet(getInternalObject().getEffectivePermissions()); } /** @@ -245,7 +234,7 @@ public class UserResource */ @Path("userGroups") public RelatedObjectSetResource getUserGroups() throws GuacamoleException { - return new RelatedObjectSetResource(user.getUserGroups()); + return new RelatedObjectSetResource(getInternalObject().getUserGroups()); } } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupDirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupDirectoryResource.java index fc4d48be3..44327a29c 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupDirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupDirectoryResource.java @@ -25,6 +25,7 @@ import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Permissions; @@ -46,6 +47,9 @@ public class UserGroupDirectoryResource extends DirectoryResource directory, DirectoryObjectTranslator translator, DirectoryObjectResourceFactory resourceFactory) { - super(userContext, directory, translator, resourceFactory); + super(authenticatedUser, userContext, UserGroup.class, directory, translator, resourceFactory); } @Override diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupResource.java index 350b59fed..6994484d9 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/usergroup/UserGroupResource.java @@ -26,6 +26,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.UserContext; @@ -43,15 +44,13 @@ import org.apache.guacamole.rest.permission.PermissionSetResource; public class UserGroupResource extends DirectoryObjectResource { - /** - * The UserGroup object represented by this UserGroupResource. - */ - private final UserGroup userGroup; - /** * Creates a new UserGroupResource which exposes the operations and * subresources available for the given UserGroup. * + * @param authenticatedUser + * The user that is accessing this resource. + * * @param userContext * The UserContext associated with the given Directory. * @@ -65,12 +64,12 @@ public class UserGroupResource * A DirectoryObjectTranslator implementation which handles Users. */ @AssistedInject - public UserGroupResource(@Assisted UserContext userContext, + public UserGroupResource(@Assisted AuthenticatedUser authenticatedUser, + @Assisted UserContext userContext, @Assisted Directory directory, @Assisted UserGroup userGroup, DirectoryObjectTranslator translator) { - super(userContext, directory, userGroup, translator); - this.userGroup = userGroup; + super(authenticatedUser, userContext, UserGroup.class, directory, userGroup, translator); } /** @@ -87,7 +86,7 @@ public class UserGroupResource */ @Path("userGroups") public RelatedObjectSetResource getUserGroups() throws GuacamoleException { - return new RelatedObjectSetResource(userGroup.getUserGroups()); + return new RelatedObjectSetResource(getInternalObject().getUserGroups()); } /** @@ -104,7 +103,7 @@ public class UserGroupResource */ @Path("memberUsers") public RelatedObjectSetResource getMemberUsers() throws GuacamoleException { - return new RelatedObjectSetResource(userGroup.getMemberUsers()); + return new RelatedObjectSetResource(getInternalObject().getMemberUsers()); } /** @@ -121,7 +120,7 @@ public class UserGroupResource */ @Path("memberUserGroups") public RelatedObjectSetResource getMemberUserGroups() throws GuacamoleException { - return new RelatedObjectSetResource(userGroup.getMemberUserGroups()); + return new RelatedObjectSetResource(getInternalObject().getMemberUserGroups()); } /** @@ -135,7 +134,7 @@ public class UserGroupResource */ @Path("permissions") public PermissionSetResource getPermissions() { - return new PermissionSetResource(userGroup); + return new PermissionSetResource(getInternalObject()); } } From 79a1bcb2a3f66edaa3ce800eaa38c4280b0fc63d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 22 Sep 2022 10:41:36 -0700 Subject: [PATCH 082/289] GUACAMOLE-1224: Extract getName() / setName() into common Nameable interface. --- .../apache/guacamole/net/auth/Connection.java | 15 +------ .../guacamole/net/auth/ConnectionGroup.java | 15 +------ .../apache/guacamole/net/auth/Nameable.java | 45 +++++++++++++++++++ .../guacamole/net/auth/SharingProfile.java | 18 +------- 4 files changed, 48 insertions(+), 45 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Nameable.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java index 3d8e64d64..24cf49c72 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connection.java @@ -34,20 +34,7 @@ import org.apache.guacamole.protocol.GuacamoleConfiguration; * backing GuacamoleConfiguration may be intentionally obfuscated or tokenized * to protect sensitive configuration information. */ -public interface Connection extends Identifiable, Connectable, Attributes { - - /** - * Returns the name assigned to this Connection. - * @return The name assigned to this Connection. - */ - public String getName(); - - /** - * Sets the name assigned to this Connection. - * - * @param name The name to assign. - */ - public void setName(String name); +public interface Connection extends Identifiable, Connectable, Attributes, Nameable { /** * Returns the unique identifier of the parent ConnectionGroup for diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionGroup.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionGroup.java index 74412de7d..a080afcc5 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionGroup.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ConnectionGroup.java @@ -26,7 +26,7 @@ import org.apache.guacamole.GuacamoleException; * Represents a connection group, which can contain both other connection groups * as well as connections. */ -public interface ConnectionGroup extends Identifiable, Connectable, Attributes { +public interface ConnectionGroup extends Identifiable, Connectable, Attributes, Nameable { /** * All legal types of connection group. @@ -51,19 +51,6 @@ public interface ConnectionGroup extends Identifiable, Connectable, Attributes { }; - /** - * Returns the name assigned to this ConnectionGroup. - * @return The name assigned to this ConnectionGroup. - */ - public String getName(); - - /** - * Sets the name assigned to this ConnectionGroup. - * - * @param name The name to assign. - */ - public void setName(String name); - /** * Returns the unique identifier of the parent ConnectionGroup for * this ConnectionGroup. diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Nameable.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Nameable.java new file mode 100644 index 000000000..d40206054 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Nameable.java @@ -0,0 +1,45 @@ +/* + * 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.auth; + +/** + * An object which has a human-readable, arbitrary name. No requirement is + * imposed by this interface regarding whether this name must be unique, + * however implementations are free to impose such restrictions. + */ +public interface Nameable { + + /** + * Returns the human-readable name assigned to this object. + * + * @return + * The name assigned to this object. + */ + String getName(); + + /** + * Sets the human-readable name assigned to this object. + * + * @param name + * The name to assign. + */ + void setName(String name); + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/SharingProfile.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/SharingProfile.java index 2d4c4326a..1fe75d912 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/SharingProfile.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/SharingProfile.java @@ -25,23 +25,7 @@ import java.util.Map; * Represents the semantics which apply to an existing connection when shared, * along with a human-readable name and unique identifier. */ -public interface SharingProfile extends Identifiable, Attributes { - - /** - * Returns the human-readable name assigned to this SharingProfile. - * - * @return - * The name assigned to this SharingProfile. - */ - public String getName(); - - /** - * Sets the human-readable name assigned to this SharingProfile. - * - * @param name - * The name to assign. - */ - public void setName(String name); +public interface SharingProfile extends Identifiable, Attributes, Nameable { /** * Returns the identifier of the primary connection associated with this From 9bda1b2c1951e471d97f25658927f452f5e33489 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 22 Sep 2022 10:41:02 -0700 Subject: [PATCH 083/289] GUACAMOLE-1224: Add a default, global event listener providing logging. --- .../guacamole/event/AffectedObject.java | 124 +++++++++++++++++ .../guacamole/event/EventLoggingListener.java | 130 ++++++++++++++++++ .../org/apache/guacamole/event/Failure.java | 56 ++++++++ .../guacamole/event/LoggableDetail.java | 39 ++++++ .../guacamole/event/RequestingUser.java | 59 ++++++++ .../guacamole/extension/ExtensionModule.java | 4 +- 6 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/Failure.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java diff --git a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java new file mode 100644 index 000000000..572a9693a --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java @@ -0,0 +1,124 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.Nameable; +import org.apache.guacamole.net.event.DirectoryEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Loggable representation of the object affected by an operation. + */ +public class AffectedObject implements LoggableDetail { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(AffectedObject.class); + + /** + * The event representing the requested operation. + */ + private final DirectoryEvent event; + + /** + * Creates a new AffectedObject representing the object affected by the + * operation described by the given event. + * + * @param event + * The event representing the operation. + */ + public AffectedObject(DirectoryEvent event) { + this.event = event; + } + + @Override + public String toString() { + + Object object = event.getObject(); + String identifier = event.getObjectIdentifier(); + + String objectType; + String name = null; // Not all objects have names + + // Obtain name of object (if applicable and available) + if (object instanceof Nameable) { + try { + name = ((Nameable) object).getName(); + } + catch (RuntimeException | Error e) { + logger.debug("Name of object \"{}\" could not be retrieved.", identifier, e); + } + } + + // Determine type of object + switch (event.getDirectoryType()) { + + // Active connections + case ACTIVE_CONNECTION: + objectType = "active connection"; + break; + + // Connections + case CONNECTION: + objectType = "connection"; + break; + + // Connection groups + case CONNECTION_GROUP: + objectType = "connection group"; + break; + + // Sharing profiles + case SHARING_PROFILE: + objectType = "sharing profile"; + break; + + // Users + case USER: + objectType = "user"; + break; + + // User groups + case USER_GROUP: + objectType = "user group"; + break; + + // Unknown + default: + objectType = (object != null) ? object.getClass().toString() : "an unknown object"; + + } + + // Describe at least the type of the object and its identifier, + // including the name of the object, as well, if available + if (identifier != null) { + if (name != null) + return objectType + " \"" + identifier + "\" (currently named \"" + name + "\")"; + else + return objectType + " \"" + identifier + "\""; + } + else + return objectType; + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java new file mode 100644 index 000000000..def5c19d9 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -0,0 +1,130 @@ +/* + * 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.event; + +import javax.annotation.Nonnull; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.net.event.DirectoryEvent; +import org.apache.guacamole.net.event.DirectoryFailureEvent; +import org.apache.guacamole.net.event.DirectorySuccessEvent; +import org.apache.guacamole.net.event.listener.Listener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Listener that records each event that occurs in the logs, such as changes + * made to objects via the REST API. + */ +public class EventLoggingListener implements Listener { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(EventLoggingListener.class); + + /** + * Logs that an operation was performed on an object within a Directory + * successfully. + * + * @param event + * The event describing the operation successfully performed on the + * object. + */ + private void logSuccess(DirectorySuccessEvent event) { + DirectoryEvent.Operation op = event.getOperation(); + switch (op) { + + case GET: + logger.debug("{} successfully accessed/retrieved {}", new RequestingUser(event), new AffectedObject(event)); + break; + + case ADD: + logger.info("{} successfully created {}", new RequestingUser(event), new AffectedObject(event)); + break; + + case UPDATE: + logger.info("{} successfully updated {}", new RequestingUser(event), new AffectedObject(event)); + break; + + case REMOVE: + logger.info("{} successfully deleted {}", new RequestingUser(event), new AffectedObject(event)); + break; + + default: + logger.warn("DirectoryEvent operation type has no corresponding log message implemented: {}", op); + logger.info("{} successfully performed an unknown action on {} {}", new RequestingUser(event), new AffectedObject(event)); + + } + } + + /** + * Logs that an operation failed to be performed on an object within a + * Directory. + * + * @param event + * The event describing the operation that failed. + */ + private void logFailure(DirectoryFailureEvent event) { + DirectoryEvent.Operation op = event.getOperation(); + switch (op) { + + case GET: + if (event.getFailure() instanceof GuacamoleResourceNotFoundException) + logger.debug("{} failed to access/retrieve {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + else + logger.info("{} failed to access/retrieve {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + case ADD: + logger.info("{} failed to create {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + case UPDATE: + logger.info("{} failed to update {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + case REMOVE: + logger.info("{} failed to delete {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + break; + + default: + logger.warn("DirectoryEvent operation type has no corresponding log message implemented: {}", op); + logger.info("{} failed to perform an unknown action on {}: {}", new RequestingUser(event), new AffectedObject(event), new Failure(event)); + + } + } + + @Override + public void handleEvent(@Nonnull Object event) throws GuacamoleException { + + if (event instanceof DirectorySuccessEvent) + logSuccess((DirectorySuccessEvent) event); + + else if (event instanceof DirectoryFailureEvent) + logFailure((DirectoryFailureEvent) event); + + else + logger.debug("Ignoring unknown/unimplemented event type: {}", + event.getClass()); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/Failure.java b/guacamole/src/main/java/org/apache/guacamole/event/Failure.java new file mode 100644 index 000000000..10b3c960b --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/Failure.java @@ -0,0 +1,56 @@ +/* + * 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.event; + +import org.apache.guacamole.net.event.FailureEvent; + +/** + * Loggable representation of a failure that occurred. + */ +public class Failure implements LoggableDetail { + + /** + * The event representing the failure. + */ + private final FailureEvent event; + + /** + * Creates a new Failure representing the failure described by the given + * event. + * + * @param event + * The event representing the failure. + */ + public Failure(FailureEvent event) { + this.event = event; + } + + @Override + public String toString() { + + Throwable failure = event.getFailure(); + if (failure == null) + return "unknown error (no specific failure recorded)"; + + return failure.getMessage(); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java b/guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java new file mode 100644 index 000000000..909412e40 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/LoggableDetail.java @@ -0,0 +1,39 @@ +/* + * 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.event; + +/** + * Provides a {@link #toString()} implementation that returns a human-readable + * string that is intended to be logged and which describes a particular detail + * of an event. + */ +public interface LoggableDetail { + + /** + * {@inheritDoc} + *

+ * A LoggableDetail implementation of toString() is required to return a + * string that is human-readable, describes a detail of a provided event, + * and that is intended to be logged. + */ + @Override + String toString(); + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java b/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java new file mode 100644 index 000000000..124aa8f80 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java @@ -0,0 +1,59 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.event.DirectoryEvent; + +/** + * Loggable representation of the user that requested an operation. + */ +public class RequestingUser implements LoggableDetail { + + /** + * The event representing the requested operation. + */ + private final DirectoryEvent event; + + /** + * Creates a new RequestingUser that represents the user that requested the + * operation described by the given event. + * + * @param event + * The event representing the requested operation. + */ + public RequestingUser(DirectoryEvent event) { + this.event = event; + } + + @Override + public String toString() { + + AuthenticatedUser user = event.getAuthenticatedUser(); + String identifier = user.getIdentifier(); + + if (AuthenticatedUser.ANONYMOUS_IDENTIFIER.equals(identifier)) + return "Anonymous user (authenticated by \"" + user.getAuthenticationProvider().getIdentifier() + "\")"; + + return "User \"" + identifier + "\" (authenticated by \"" + user.getAuthenticationProvider().getIdentifier() + "\")"; + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java index 0fcff57dd..e5fbe2e57 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java @@ -35,6 +35,7 @@ import org.apache.guacamole.auth.file.FileAuthenticationProvider; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.event.EventLoggingListener; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.event.listener.Listener; import org.apache.guacamole.properties.StringSetProperty; @@ -628,8 +629,9 @@ public class ExtensionModule extends ServletModule { final Set toleratedAuthProviders = getToleratedAuthenticationProviders(); loadExtensions(javaScriptResources, cssResources, toleratedAuthProviders); - // Always bind default file-driven auth last + // Always bind default file-driven auth and event logging last bindAuthenticationProvider(FileAuthenticationProvider.class, toleratedAuthProviders); + bindListener(EventLoggingListener.class); // Dynamically generate app.js and app.css from extensions serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources))); From 8c36eaf55da22ac07dac9cda0c608bb5c94f4cb2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 2 Oct 2022 11:19:01 -0700 Subject: [PATCH 084/289] GUACAMOLE-1224: Add events for overall webapp startup/shutdown. --- .../net/event/ApplicationShutdownEvent.java | 36 ++++++++++++++++ .../net/event/ApplicationStartedEvent.java | 29 +++++++++++++ .../GuacamoleServletContextListener.java | 43 +++++++++++++++++-- .../guacamole/event/EventLoggingListener.java | 11 ++++- 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationShutdownEvent.java create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationStartedEvent.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationShutdownEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationShutdownEvent.java new file mode 100644 index 000000000..ba7f50bac --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationShutdownEvent.java @@ -0,0 +1,36 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.event.listener.Listener; + +/** + * Event that is dispatched when the web application has nearly completely shut + * down, including the authentication/authorization portion of extensions. Any + * installed extensions are still loaded (such that they may receive this event + * via {@link Listener#handleEvent(java.lang.Object)}, but their authentication + * providers will have been shut down via {@link AuthenticationProvider#shutdown()}, + * and resources from user sessions will have been closed and released via + * {@link UserContext#invalidate()}. + */ +public interface ApplicationShutdownEvent { +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationStartedEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationStartedEvent.java new file mode 100644 index 000000000..cf2ae2f3b --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/ApplicationStartedEvent.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.net.event; + +/** + * Event that is dispatched when the web application has finished starting up, + * including all extensions. This event indicates only that the web application + * startup process has completed and all extensions have been loaded. It does + * not indicate whether all extensions have started successfully. + */ +public interface ApplicationStartedEvent { +} diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java index 2b3f155d0..1068477c4 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java @@ -35,11 +35,14 @@ import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.extension.ExtensionModule; import org.apache.guacamole.log.LogModule; import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.event.ApplicationShutdownEvent; +import org.apache.guacamole.net.event.ApplicationStartedEvent; import org.apache.guacamole.properties.BooleanGuacamoleProperty; import org.apache.guacamole.properties.FileGuacamoleProperties; import org.apache.guacamole.rest.RESTServiceModule; import org.apache.guacamole.rest.auth.HashTokenSessionMap; import org.apache.guacamole.rest.auth.TokenSessionMap; +import org.apache.guacamole.rest.event.ListenerService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -125,7 +128,13 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener */ @Inject private List temporaryFiles; - + + /** + * Service for dispatching events to registered listeners. + */ + @Inject + private ListenerService listenerService; + /** * Internal reference to the Guice injector that was lazily created when * getInjector() was first invoked. @@ -179,6 +188,17 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener // Store reference to injector for use by Jersey and HK2 bridge servletContextEvent.getServletContext().setAttribute(GUICE_INJECTOR, injector); + // Inform any listeners that application startup has completed + try { + listenerService.handleEvent(new ApplicationStartedEvent() { + // The application startup event currently has no content + }); + } + catch (GuacamoleException e) { + logger.error("An extension listening for application startup failed: {}", e.getMessage()); + logger.debug("Extension failed internally while handling the application startup event.", e); + } + } @Override @@ -228,19 +248,36 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener // Clean up reference to Guice injector servletContextEvent.getServletContext().removeAttribute(GUICE_INJECTOR); - // Shutdown TokenSessionMap + // Shutdown TokenSessionMap, invalidating all sessions (logging all + // users out) if (sessionMap != null) sessionMap.shutdown(); - // Unload all extensions + // Unload authentication for all extensions if (authProviders != null) { for (AuthenticationProvider authProvider : authProviders) authProvider.shutdown(); } + // Inform any listeners that application shutdown has completed + try { + listenerService.handleEvent(new ApplicationShutdownEvent() { + // The application shutdown event currently has no content + }); + } + catch (GuacamoleException e) { + logger.error("An extension listening for application shutdown failed: {}", e.getMessage()); + logger.debug("Extension failed internally while handling the application shutdown event.", e); + } + } finally { + // NOTE: This temporary file cleanup must happen AFTER firing the + // ApplicationShutdownEvent, or an extension that relies on a .jar + // file among those temporary files might fail internally when + // attempting to process the event. + // Regardless of what may succeed/fail here, always attempt to // clean up ALL temporary files if (temporaryFiles != null) diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java index def5c19d9..f3b804892 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -22,6 +22,8 @@ package org.apache.guacamole.event; import javax.annotation.Nonnull; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.net.event.ApplicationShutdownEvent; +import org.apache.guacamole.net.event.ApplicationStartedEvent; import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent; import org.apache.guacamole.net.event.DirectorySuccessEvent; @@ -115,12 +117,19 @@ public class EventLoggingListener implements Listener { @Override public void handleEvent(@Nonnull Object event) throws GuacamoleException { + // General object creation/modification/deletion if (event instanceof DirectorySuccessEvent) logSuccess((DirectorySuccessEvent) event); - else if (event instanceof DirectoryFailureEvent) logFailure((DirectoryFailureEvent) event); + // Application startup/shutdown + else if (event instanceof ApplicationStartedEvent) + logger.info("The Apache Guacamole web application has started."); + else if (event instanceof ApplicationShutdownEvent) + logger.info("The Apache Guacamole web application has shut down."); + + // Unknown events else logger.debug("Ignoring unknown/unimplemented event type: {}", event.getClass()); From 63de886e5d81e802ce317b4f26d8f01cea297059 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 2 Oct 2022 11:20:04 -0700 Subject: [PATCH 085/289] GUACAMOLE-1224: Ensure all user sessions are explicitly invalidated prior to application shutdown. --- .../org/apache/guacamole/rest/auth/HashTokenSessionMap.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java index a344ab911..24f653098 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java @@ -225,7 +225,13 @@ public class HashTokenSessionMap implements TokenSessionMap { @Override public void shutdown() { + + // Terminate the automatic session invalidation thread executor.shutdownNow(); + + // Forcibly invalidate any remaining sessions + sessionMap.values().stream().forEach(GuacamoleSession::invalidate); + } } From b3319b817d57dfb46f87d879130c85bfc2921afe Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 2 Oct 2022 11:58:32 -0700 Subject: [PATCH 086/289] GUACAMOLE-1224: Add user session invalidation/logout event. --- .../event/UserSessionInvalidatedEvent.java | 27 ++++++++++++++ .../apache/guacamole/GuacamoleSession.java | 37 +++++++++++++++---- .../guacamole/event/EventLoggingListener.java | 6 +++ .../guacamole/event/RequestingUser.java | 6 +-- .../rest/auth/AuthenticationService.java | 9 +---- 5 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/UserSessionInvalidatedEvent.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/UserSessionInvalidatedEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/UserSessionInvalidatedEvent.java new file mode 100644 index 000000000..4398cfa89 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/UserSessionInvalidatedEvent.java @@ -0,0 +1,27 @@ +/* + * 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.event; + +/** + * Event that is dispatched when a user has logged out or their session has + * expired. + */ +public interface UserSessionInvalidatedEvent extends UserEvent { +} diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java index 24ea196c5..3e8d488c3 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java @@ -23,12 +23,13 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.apache.guacamole.environment.Environment; import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.event.UserSessionInvalidatedEvent; import org.apache.guacamole.rest.auth.DecoratedUserContext; +import org.apache.guacamole.rest.event.ListenerService; import org.apache.guacamole.tunnel.UserTunnel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,8 +59,12 @@ public class GuacamoleSession { /** * All currently-active tunnels, indexed by tunnel UUID. */ - private final Map tunnels = - new ConcurrentHashMap(); + private final Map tunnels = new ConcurrentHashMap<>(); + + /** + * Service for dispatching events to registered event listeners. + */ + private final ListenerService listenerService; /** * The last time this session was accessed. @@ -70,9 +75,9 @@ public class GuacamoleSession { * Creates a new Guacamole session associated with the given * AuthenticatedUser and UserContexts. * - * @param environment - * The environment of the Guacamole server associated with this new - * session. + * @param listenerService + * The service to use to notify registered event listeners when this + * session is invalidated. * * @param authenticatedUser * The authenticated user to associate this session with. @@ -83,11 +88,12 @@ public class GuacamoleSession { * @throws GuacamoleException * If an error prevents the session from being created. */ - public GuacamoleSession(Environment environment, + public GuacamoleSession(ListenerService listenerService, AuthenticatedUser authenticatedUser, List userContexts) throws GuacamoleException { this.lastAccessedTime = System.currentTimeMillis(); + this.listenerService = listenerService; this.authenticatedUser = authenticatedUser; this.userContexts = userContexts; } @@ -260,6 +266,23 @@ public class GuacamoleSession { // Invalidate the authenticated user object authenticatedUser.invalidate(); + // Advise any registered listeners that the user's session is now + // invalidated + try { + listenerService.handleEvent(new UserSessionInvalidatedEvent() { + + @Override + public AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + + }); + } + catch (GuacamoleException e) { + logger.error("An extension listening for session invalidation failed: {}", e.getMessage()); + logger.debug("Extension failed internally while handling the session invalidation event.", e); + } + } } diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java index f3b804892..85d616cbb 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -27,6 +27,7 @@ import org.apache.guacamole.net.event.ApplicationStartedEvent; import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent; import org.apache.guacamole.net.event.DirectorySuccessEvent; +import org.apache.guacamole.net.event.UserSessionInvalidatedEvent; import org.apache.guacamole.net.event.listener.Listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -123,6 +124,11 @@ public class EventLoggingListener implements Listener { else if (event instanceof DirectoryFailureEvent) logFailure((DirectoryFailureEvent) event); + // Logout / session expiration + else if (event instanceof UserSessionInvalidatedEvent) + logger.info("{} has logged out, or their session has expired or " + + "been terminated.", new RequestingUser((UserSessionInvalidatedEvent) event)); + // Application startup/shutdown else if (event instanceof ApplicationStartedEvent) logger.info("The Apache Guacamole web application has started."); diff --git a/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java b/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java index 124aa8f80..bcc14f4c4 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/RequestingUser.java @@ -20,7 +20,7 @@ package org.apache.guacamole.event; import org.apache.guacamole.net.auth.AuthenticatedUser; -import org.apache.guacamole.net.event.DirectoryEvent; +import org.apache.guacamole.net.event.UserEvent; /** * Loggable representation of the user that requested an operation. @@ -30,7 +30,7 @@ public class RequestingUser implements LoggableDetail { /** * The event representing the requested operation. */ - private final DirectoryEvent event; + private final UserEvent event; /** * Creates a new RequestingUser that represents the user that requested the @@ -39,7 +39,7 @@ public class RequestingUser implements LoggableDetail { * @param event * The event representing the requested operation. */ - public RequestingUser(DirectoryEvent event) { + public RequestingUser(UserEvent event) { this.event = event; } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java index 705d7a856..48a2d5a36 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java @@ -30,7 +30,6 @@ import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleUnauthorizedException; import org.apache.guacamole.GuacamoleSession; -import org.apache.guacamole.environment.Environment; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; @@ -56,12 +55,6 @@ public class AuthenticationService { */ private static final Logger logger = LoggerFactory.getLogger(AuthenticationService.class); - /** - * The Guacamole server environment. - */ - @Inject - private Environment environment; - /** * All configured authentication providers which can be used to * authenticate users or retrieve data associated with authenticated users. @@ -443,7 +436,7 @@ public class AuthenticationService { // If no existing session, generate a new token/session pair else { authToken = authTokenGenerator.getToken(); - tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts)); + tokenSessionMap.put(authToken, new GuacamoleSession(listenerService, authenticatedUser, userContexts)); logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier()); } From 818471ac8489f9b75452c99e1a356412dad2425b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 3 Oct 2022 13:41:54 -0700 Subject: [PATCH 087/289] GUACAMOLE-1224: Migrate existing auth-related logging to global event listener. --- .../InMemoryAuthenticationFailureTracker.java | 37 +------- .../guacamole/net/auth/Credentials.java | 32 +++++++ .../net/event/AuthenticationSuccessEvent.java | 49 +++++++++- .../guacamole/event/EventLoggingListener.java | 56 ++++++++++- .../apache/guacamole/event/RemoteAddress.java | 95 +++++++++++++++++++ .../rest/auth/AuthenticationService.java | 78 +-------------- 6 files changed, 233 insertions(+), 114 deletions(-) create mode 100644 guacamole/src/main/java/org/apache/guacamole/event/RemoteAddress.java diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java index b655168e1..58ed4e014 100644 --- a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java +++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java @@ -21,7 +21,6 @@ package org.apache.guacamole.auth.ban.status; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.language.TranslatableGuacamoleClientTooManyException; @@ -88,40 +87,6 @@ public class InMemoryAuthenticationFailureTracker implements AuthenticationFailu } - /** - * Returns whether the given Credentials do not contain any specific - * authentication parameters, including HTTP parameters. An authentication - * request that contains no parameters whatsoever will tend to be the - * first, anonymous, credential-less authentication attempt that results in - * the initial login screen rendering. - * - * @param credentials - * The Credentials object to test. - * - * @return - * true if the given Credentials contain no authentication parameters - * whatsoever, false otherwise. - */ - private boolean isEmpty(Credentials credentials) { - - // An authentication request that contains an explicit username or - // password (even if blank) is non-empty, regardless of how the values - // were passed - if (credentials.getUsername() != null || credentials.getPassword() != null) - return false; - - // All further tests depend on HTTP request details - HttpServletRequest request = credentials.getRequest(); - if (request == null) - return true; - - // An authentication request is non-empty if it contains any HTTP - // parameters at all or contains an authentication token - return !request.getParameterNames().hasMoreElements() - && request.getHeader("Guacamole-Token") == null; - - } - /** * Reports that the given address has just failed to authenticate and * returns the AuthenticationFailureStatus that represents that failure. If @@ -168,7 +133,7 @@ public class InMemoryAuthenticationFailureTracker implements AuthenticationFailu boolean failed) throws GuacamoleException { // Ignore requests that do not contain explicit parameters of any kind - if (isEmpty(credentials)) + if (credentials.isEmpty()) return; // Determine originating address of the authentication request diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java index 40f991244..6ad0e240b 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Credentials.java @@ -236,4 +236,36 @@ public class Credentials implements Serializable { this.remoteHostname = remoteHostname; } + /** + * Returns whether this Credentials object does not contain any specific + * authentication parameters, including HTTP parameters and the HTTP header + * used for the authentication token. An authentication request that + * contains no parameters whatsoever will tend to be the first, anonymous, + * credential-less authentication attempt that results in the initial login + * screen rendering. + * + * @return + * true if this Credentials object contains no authentication + * parameters whatsoever, false otherwise. + */ + public boolean isEmpty() { + + // An authentication request that contains an explicit username or + // password (even if blank) is non-empty, regardless of how the values + // were passed + if (getUsername() != null || getPassword() != null) + return false; + + // All further tests depend on HTTP request details + HttpServletRequest httpRequest = getRequest(); + if (httpRequest == null) + return true; + + // An authentication request is non-empty if it contains any HTTP + // parameters at all or contains an authentication token + return !httpRequest.getParameterNames().hasMoreElements() + && httpRequest.getHeader("Guacamole-Token") == null; + + } + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java index a9b21dce8..a7270968e 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java @@ -42,17 +42,47 @@ public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent, */ private final AuthenticatedUser authenticatedUser; + /** + * Whether the successful authentication attempt represented by this event + * is related to an established Guacamole session. + */ + private final boolean existingSession; + /** * Creates a new AuthenticationSuccessEvent which represents a successful * authentication attempt by the user identified by the given - * AuthenticatedUser object. + * AuthenticatedUser object. The authentication attempt is presumed to be + * a fresh authentication attempt unrelated to an established session (a + * login attempt). * * @param authenticatedUser * The AuthenticatedUser identifying the user that successfully * authenticated. */ public AuthenticationSuccessEvent(AuthenticatedUser authenticatedUser) { + this(authenticatedUser, false); + } + + /** + * Creates a new AuthenticationSuccessEvent which represents a successful + * authentication attempt by the user identified by the given + * AuthenticatedUser object. Whether the authentication attempt is + * related to an established session (a periodic re-authentication attempt + * that updates session status) or not (a fresh login attempt) is + * determined by the value of the provided flag. + * + * @param authenticatedUser + * The AuthenticatedUser identifying the user that successfully + * authenticated. + * + * @param existingSession + * Whether this AuthenticationSuccessEvent represents an + * re-authentication attempt that updates the status of an established + * Guacamole session. + */ + public AuthenticationSuccessEvent(AuthenticatedUser authenticatedUser, boolean existingSession) { this.authenticatedUser = authenticatedUser; + this.existingSession = existingSession; } @Override @@ -70,4 +100,21 @@ public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent, return getAuthenticatedUser().getAuthenticationProvider(); } + /** + * Returns whether the successful authentication attempt represented by + * this event is related to an established Guacamole session. During normal + * operation, the Guacamole web application will periodically + * re-authenticate with the server to verify its authentication token and + * update the session state, in which case the value returned by this + * function will be true. If the user was not already authenticated and has + * just initially logged in, false is returned. + * + * @return + * true if this AuthenticationSuccessEvent is related to a Guacamole + * session that was already established, false otherwise. + */ + public boolean isExistingSession() { + return existingSession; + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java index 85d616cbb..2cf3a70d6 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -22,8 +22,12 @@ package org.apache.guacamole.event; import javax.annotation.Nonnull; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.event.ApplicationShutdownEvent; import org.apache.guacamole.net.event.ApplicationStartedEvent; +import org.apache.guacamole.net.event.AuthenticationFailureEvent; +import org.apache.guacamole.net.event.AuthenticationSuccessEvent; import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent; import org.apache.guacamole.net.event.DirectorySuccessEvent; @@ -115,6 +119,52 @@ public class EventLoggingListener implements Listener { } } + /** + * Logs that authentication succeeded for a user. + * + * @param event + * The event describing the authentication attempt that succeeded. + */ + private void logSuccess(AuthenticationSuccessEvent event) { + if (!event.isExistingSession()) + logger.info("{} successfully authenticated from {}", + new RequestingUser(event), + new RemoteAddress(event.getCredentials())); + else + logger.debug("{} successfully re-authenticated their existing " + + "session from {}", new RequestingUser(event), + new RemoteAddress(event.getCredentials())); + } + + /** + * Logs that authentication failed for a user. + * + * @param event + * The event describing the authentication attempt that failed. + */ + private void logFailure(AuthenticationFailureEvent event) { + + Credentials creds = event.getCredentials(); + String username = creds.getUsername(); + + if (creds.isEmpty()) + logger.debug("Empty authentication attempt (login screen " + + "initialization) from {} failed: {}", + new RemoteAddress(creds), new Failure(event)); + else if (username == null || username.isEmpty()) + logger.debug("Anonymous authentication attempt from {} failed: {}", + new RemoteAddress(creds), new Failure(event)); + else if (event.getFailure() instanceof GuacamoleInsufficientCredentialsException) + logger.debug("Authentication attempt from {} for user \"{}\" " + + "requires additional credentials to continue: {}", + new RemoteAddress(creds), username, new Failure(event)); + else + logger.warn("Authentication attempt from {} for user \"{}\" " + + "failed: {}", new RemoteAddress(creds), username, + new Failure(event)); + + } + @Override public void handleEvent(@Nonnull Object event) throws GuacamoleException { @@ -124,7 +174,11 @@ public class EventLoggingListener implements Listener { else if (event instanceof DirectoryFailureEvent) logFailure((DirectoryFailureEvent) event); - // Logout / session expiration + // Login / logout / session expiration + else if (event instanceof AuthenticationSuccessEvent) + logSuccess((AuthenticationSuccessEvent) event); + else if (event instanceof AuthenticationFailureEvent) + logFailure((AuthenticationFailureEvent) event); else if (event instanceof UserSessionInvalidatedEvent) logger.info("{} has logged out, or their session has expired or " + "been terminated.", new RequestingUser((UserSessionInvalidatedEvent) event)); diff --git a/guacamole/src/main/java/org/apache/guacamole/event/RemoteAddress.java b/guacamole/src/main/java/org/apache/guacamole/event/RemoteAddress.java new file mode 100644 index 000000000..f55602f59 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/event/RemoteAddress.java @@ -0,0 +1,95 @@ +/* + * 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.event; + +import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; +import org.apache.guacamole.net.auth.Credentials; + +/** + * Loggable representation of the remote address of a user, including any + * intervening proxies noted by "X-Forwarded-For". This representation takes + * into account the fact that "X-Forwarded-For" may come from an untrusted + * source, logging such addresses within square brackets alongside the trusted + * source IP. + */ +public class RemoteAddress implements LoggableDetail { + + /** + * Regular expression which matches any IPv4 address. + */ + private static final String IPV4_ADDRESS_REGEX = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})"; + + /** + * Regular expression which matches any IPv6 address. + */ + private static final String IPV6_ADDRESS_REGEX = "([0-9a-fA-F]*(:[0-9a-fA-F]*){0,7})"; + + /** + * Regular expression which matches any IP address, regardless of version. + */ + private static final String IP_ADDRESS_REGEX = "(" + IPV4_ADDRESS_REGEX + "|" + IPV6_ADDRESS_REGEX + ")"; + + /** + * Regular expression which matches any Port Number. + */ + private static final String PORT_NUMBER_REGEX = "(:[0-9]{1,5})?"; + + /** + * Pattern which matches valid values of the de-facto standard + * "X-Forwarded-For" header. + */ + private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + PORT_NUMBER_REGEX + "(, " + IP_ADDRESS_REGEX + PORT_NUMBER_REGEX + ")*$"); + + /** + * The credentials supplied by the user when they authenticated. + */ + private final Credentials creds; + + /** + * Creates a new RemoteAddress representing the source address of the HTTP + * request that provided the given Credentials. + * + * @param creds + * The Credentials associated with the request whose source address + * should be represented by this RemoteAddress. + */ + public RemoteAddress(Credentials creds) { + this.creds = creds; + } + + @Override + public String toString() { + + HttpServletRequest request = creds.getRequest(); + if (request == null) + return creds.getRemoteAddress(); + + // Log X-Forwarded-For, if present and valid + String header = request.getHeader("X-Forwarded-For"); + if (header != null && X_FORWARDED_FOR.matcher(header).matches()) + return "[" + header + ", " + request.getRemoteAddr() + "]"; + + // If header absent or invalid, just use source IP + return request.getRemoteAddr(); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java index 48a2d5a36..305073de3 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java @@ -21,9 +21,7 @@ package org.apache.guacamole.rest.auth; import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; @@ -98,57 +96,6 @@ public class AuthenticationService { */ public static final String TOKEN_PARAMETER_NAME = "token"; - /** - * Regular expression which matches any IPv4 address. - */ - private static final String IPV4_ADDRESS_REGEX = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})"; - - /** - * Regular expression which matches any IPv6 address. - */ - private static final String IPV6_ADDRESS_REGEX = "([0-9a-fA-F]*(:[0-9a-fA-F]*){0,7})"; - - /** - * Regular expression which matches any IP address, regardless of version. - */ - private static final String IP_ADDRESS_REGEX = "(" + IPV4_ADDRESS_REGEX + "|" + IPV6_ADDRESS_REGEX + ")"; - - /** - * Regular expression which matches any Port Number. - */ - private static final String PORT_NUMBER_REGEX = "(:[0-9]{1,5})?"; - - /** - * Pattern which matches valid values of the de-facto standard - * "X-Forwarded-For" header. - */ - private static final Pattern X_FORWARDED_FOR = Pattern.compile("^" + IP_ADDRESS_REGEX + PORT_NUMBER_REGEX + "(, " + IP_ADDRESS_REGEX + PORT_NUMBER_REGEX + ")*$"); - - /** - * Returns a formatted string containing an IP address, or list of IP - * addresses, which represent the HTTP client and any involved proxies. As - * the headers used to determine proxies can easily be forged, this data is - * superficially validated to ensure that it at least looks like a list of - * IPs. - * - * @param request - * The HTTP request to format. - * - * @return - * A formatted string containing one or more IP addresses. - */ - private String getLoggableAddress(HttpServletRequest request) { - - // Log X-Forwarded-For, if present and valid - String header = request.getHeader("X-Forwarded-For"); - if (header != null && X_FORWARDED_FOR.matcher(header).matches()) - return "[" + header + ", " + request.getRemoteAddr() + "]"; - - // If header absent or invalid, just use source IP - return request.getRemoteAddr(); - - } - /** * Attempts authentication against all AuthenticationProviders, in order, * using the provided credentials. The first authentication failure takes @@ -437,12 +384,12 @@ public class AuthenticationService { else { authToken = authTokenGenerator.getToken(); tokenSessionMap.put(authToken, new GuacamoleSession(listenerService, authenticatedUser, userContexts)); - logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier()); } // Report authentication success try { - listenerService.handleEvent(new AuthenticationSuccessEvent(authenticatedUser)); + listenerService.handleEvent(new AuthenticationSuccessEvent(authenticatedUser, + existingSession != null)); } catch (GuacamoleException e) { throw new GuacamoleAuthenticationProcessException("User " @@ -454,25 +401,9 @@ public class AuthenticationService { // Log and rethrow any authentication errors catch (GuacamoleAuthenticationProcessException e) { - // Get request and username for sake of logging - HttpServletRequest request = credentials.getRequest(); - String username = credentials.getUsername(); - listenerService.handleEvent(new AuthenticationFailureEvent(credentials, e.getAuthenticationProvider(), e.getCause())); - // Log authentication failures with associated usernames - if (username != null) { - if (logger.isWarnEnabled()) - logger.warn("Authentication attempt from {} for user \"{}\" failed.", - getLoggableAddress(request), username); - } - - // Log anonymous authentication failures - else if (logger.isDebugEnabled()) - logger.debug("Anonymous authentication attempt from {} failed.", - getLoggableAddress(request)); - // Rethrow exception e.rethrowCause(); @@ -490,11 +421,6 @@ public class AuthenticationService { } - if (logger.isInfoEnabled()) - logger.info("User \"{}\" successfully authenticated from {}.", - authenticatedUser.getIdentifier(), - getLoggableAddress(credentials.getRequest())); - return authToken; } From e5761551e4c9a4d2d997cbe8fcebc740f363765c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 3 Oct 2022 15:57:11 -0700 Subject: [PATCH 088/289] GUACAMOLE-1224: Log identifiers of extensions reporting authentication failures. --- .../guacamole/event/EventLoggingListener.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java index 2cf3a70d6..90a929765 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -22,6 +22,7 @@ package org.apache.guacamole.event; import javax.annotation.Nonnull; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.event.ApplicationShutdownEvent; @@ -144,6 +145,8 @@ public class EventLoggingListener implements Listener { */ private void logFailure(AuthenticationFailureEvent event) { + AuthenticationProvider authProvider = event.getAuthenticationProvider(); + Credentials creds = event.getCredentials(); String username = creds.getUsername(); @@ -154,14 +157,27 @@ public class EventLoggingListener implements Listener { else if (username == null || username.isEmpty()) logger.debug("Anonymous authentication attempt from {} failed: {}", new RemoteAddress(creds), new Failure(event)); - else if (event.getFailure() instanceof GuacamoleInsufficientCredentialsException) - logger.debug("Authentication attempt from {} for user \"{}\" " - + "requires additional credentials to continue: {}", - new RemoteAddress(creds), username, new Failure(event)); - else - logger.warn("Authentication attempt from {} for user \"{}\" " - + "failed: {}", new RemoteAddress(creds), username, - new Failure(event)); + else if (event.getFailure() instanceof GuacamoleInsufficientCredentialsException) { + if (authProvider != null) + logger.debug("Authentication attempt from {} for user \"{}\" " + + "requires additional credentials to continue: {} " + + "(requested by \"{}\")", new RemoteAddress(creds), + username, new Failure(event), authProvider.getIdentifier()); + else + logger.debug("Authentication attempt from {} for user \"{}\" " + + "requires additional credentials to continue: {}", + new RemoteAddress(creds), username, new Failure(event)); + } + else { + if (authProvider != null) + logger.warn("Authentication attempt from {} for user \"{}\" " + + "failed: {} (rejected by \"{}\")", new RemoteAddress(creds), + username, new Failure(event), authProvider.getIdentifier()); + else + logger.warn("Authentication attempt from {} for user \"{}\" " + + "failed: {}", new RemoteAddress(creds), username, + new Failure(event)); + } } From 93e74224667c0cef73d176f464ea1490a3c30889 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 4 Oct 2022 14:15:48 -0700 Subject: [PATCH 089/289] GUACAMOLE-1224: Extract base interface for events affecting objects stored in a Directory. --- .../guacamole/net/event/DirectoryEvent.java | 41 +++++------- .../net/event/DirectoryObjectEvent.java | 65 +++++++++++++++++++ .../guacamole/event/AffectedObject.java | 6 +- 3 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java index 2ff74bf94..725c75048 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java @@ -24,13 +24,14 @@ import org.apache.guacamole.net.auth.Identifiable; /** * Abstract basis for events which involve a modification made to the objects - * within a {@link Directory}. + * within a {@link Directory} through the operations exposed by the Directory + * interface. * * @param * The type of object stored within the {@link Directory}. */ public interface DirectoryEvent - extends AuthenticationProviderEvent, UserEvent { + extends DirectoryObjectEvent { /** * The types of directory operations that may be represented by a @@ -70,15 +71,6 @@ public interface DirectoryEvent } - /** - * Returns the type of objects stored within the {@link Directory} - * affected by the operation. - * - * @return - * The type of objects stored within the {@link Directory}. - */ - Directory.Type getDirectoryType(); - /** * Returns the operation that was performed/attempted. * @@ -88,24 +80,25 @@ public interface DirectoryEvent Operation getOperation(); /** - * Returns the identifier of the object affected by the operation. If the - * object was just created, this will be the identifier of the new object. - * - * @return - * The identifier of the object affected by the operation. + * {@inheritDoc} + *

+ * If the object was just created, this will be the identifier of the new + * object. */ + @Override String getObjectIdentifier(); /** - * Returns the object affected by the operation, if available. For - * deletions, there is no guarantee that the affected object will be - * available within this event. If the object is not available, null is - * returned. - * - * @return - * The object affected by the operation performed, or null if that - * object is not available in the context of this event. + * {@inheritDoc} + *

+ * Currently, for object creation ({@link Operation#ADD}), retrieval + * ({@link Operation#GET}), modification and ({@link Operation#UPDATE}), + * it can be expected that the affected object will be available, however + * the caller should verify this regardless. For deletions + * ({@link Operation#REMOVE}), the object can only be made available for + * single deletions, and cannot be made available for batch deletions. */ + @Override ObjectType getObject(); } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java new file mode 100644 index 000000000..812fcbe60 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java @@ -0,0 +1,65 @@ +/* + * 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.event; + +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.Identifiable; + +/** + * Abstract basis for events which affect or directly relate to objects that + * may be stored within a {@link Directory}. + * + * @param + * The type of object stored within the {@link Directory}. + */ +public interface DirectoryObjectEvent + extends AuthenticationProviderEvent, UserEvent { + + /** + * Returns the type of {@link Directory} that contains the object affected + * by the operation. + * + * @return + * The type of objects stored within the {@link Directory}. + */ + Directory.Type getDirectoryType(); + + /** + * Returns the identifier of the object affected by the operation. + * + * @return + * The identifier of the object affected by the operation. + */ + String getObjectIdentifier(); + + /** + * Returns the object affected by the operation, if available. Whether the + * affected object is available is context- and implementation-dependent. + * There is no general guarantee across all implementations of this event + * that the affected object will be available. If the object is not + * available, null is returned. + * + * @return + * The object affected by the operation performed, or null if that + * object is not available in the context of this event. + */ + ObjectType getObject(); + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java index 572a9693a..aec678be2 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java @@ -20,9 +20,9 @@ package org.apache.guacamole.event; import org.apache.guacamole.net.auth.Nameable; -import org.apache.guacamole.net.event.DirectoryEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.guacamole.net.event.DirectoryObjectEvent; /** * Loggable representation of the object affected by an operation. @@ -37,7 +37,7 @@ public class AffectedObject implements LoggableDetail { /** * The event representing the requested operation. */ - private final DirectoryEvent event; + private final DirectoryObjectEvent event; /** * Creates a new AffectedObject representing the object affected by the @@ -46,7 +46,7 @@ public class AffectedObject implements LoggableDetail { * @param event * The event representing the operation. */ - public AffectedObject(DirectoryEvent event) { + public AffectedObject(DirectoryObjectEvent event) { this.event = event; } From 9b7982a7f2b4c93aab3874386eaa84ae5362def0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 4 Oct 2022 15:11:20 -0700 Subject: [PATCH 090/289] GUACAMOLE-1224: Clarify context of getAuthenticationProvider() for DirectoryObjectEvents. --- .../guacamole/net/event/DirectoryObjectEvent.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java index 812fcbe60..62dc36a6f 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java @@ -19,6 +19,8 @@ package org.apache.guacamole.net.event; +import org.apache.guacamole.net.auth.AuthenticatedUser; +import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Identifiable; @@ -32,6 +34,18 @@ import org.apache.guacamole.net.auth.Identifiable; public interface DirectoryObjectEvent extends AuthenticationProviderEvent, UserEvent { + /** + * {@inheritDoc} + *

+ * NOTE: For subclasses of {@link DirectoryObjectEvent}, this will be the + * AuthenticationProvider associated with the affected object. This is not + * necessarily the same as the AuthenticationProvider that authenticated + * the user performing the operation, which can be retrieved via + * {@link #getAuthenticatedUser()} and {@link AuthenticatedUser#getAuthenticationProvider()}. + */ + @Override + AuthenticationProvider getAuthenticationProvider(); + /** * Returns the type of {@link Directory} that contains the object affected * by the operation. From a0a5335c4d887c7e9586baf51d353809616db946 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 5 Oct 2022 09:09:28 -0700 Subject: [PATCH 091/289] GUACAMOLE-1224: Further clarify DirectoryObjectEvent, renaming to IdentifiableObjectEvent. --- .../guacamole/net/event/DirectoryEvent.java | 2 +- ...vent.java => IdentifiableObjectEvent.java} | 21 ++++++++++--------- .../guacamole/event/AffectedObject.java | 6 +++--- 3 files changed, 15 insertions(+), 14 deletions(-) rename guacamole-ext/src/main/java/org/apache/guacamole/net/event/{DirectoryObjectEvent.java => IdentifiableObjectEvent.java} (75%) diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java index 725c75048..5e9116496 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java @@ -31,7 +31,7 @@ import org.apache.guacamole.net.auth.Identifiable; * The type of object stored within the {@link Directory}. */ public interface DirectoryEvent - extends DirectoryObjectEvent { + extends IdentifiableObjectEvent { /** * The types of directory operations that may be represented by a diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/IdentifiableObjectEvent.java similarity index 75% rename from guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java rename to guacamole-ext/src/main/java/org/apache/guacamole/net/event/IdentifiableObjectEvent.java index 62dc36a6f..69ecfeaa4 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryObjectEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/IdentifiableObjectEvent.java @@ -26,29 +26,30 @@ import org.apache.guacamole.net.auth.Identifiable; /** * Abstract basis for events which affect or directly relate to objects that - * may be stored within a {@link Directory}. + * are {@link Identifiable} and may be stored within a {@link Directory}. * * @param - * The type of object stored within the {@link Directory}. + * The type of object affected or related to the event. */ -public interface DirectoryObjectEvent +public interface IdentifiableObjectEvent extends AuthenticationProviderEvent, UserEvent { /** * {@inheritDoc} *

- * NOTE: For subclasses of {@link DirectoryObjectEvent}, this will be the - * AuthenticationProvider associated with the affected object. This is not - * necessarily the same as the AuthenticationProvider that authenticated - * the user performing the operation, which can be retrieved via - * {@link #getAuthenticatedUser()} and {@link AuthenticatedUser#getAuthenticationProvider()}. + * NOTE: For subclasses of {@link IdentifiableObjectEvent}, this will be the + * AuthenticationProvider associated with the affected, {@link Identifiable} + * object. This is not necessarily the same as the AuthenticationProvider + * that authenticated the user performing the operation, which can be + * retrieved via {@link #getAuthenticatedUser()} and + * {@link AuthenticatedUser#getAuthenticationProvider()}. */ @Override AuthenticationProvider getAuthenticationProvider(); /** - * Returns the type of {@link Directory} that contains the object affected - * by the operation. + * Returns the type of {@link Directory} that may contains the object + * affected by the operation. * * @return * The type of objects stored within the {@link Directory}. diff --git a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java index aec678be2..b95122bae 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java @@ -22,7 +22,7 @@ package org.apache.guacamole.event; import org.apache.guacamole.net.auth.Nameable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.guacamole.net.event.DirectoryObjectEvent; +import org.apache.guacamole.net.event.IdentifiableObjectEvent; /** * Loggable representation of the object affected by an operation. @@ -37,7 +37,7 @@ public class AffectedObject implements LoggableDetail { /** * The event representing the requested operation. */ - private final DirectoryObjectEvent event; + private final IdentifiableObjectEvent event; /** * Creates a new AffectedObject representing the object affected by the @@ -46,7 +46,7 @@ public class AffectedObject implements LoggableDetail { * @param event * The event representing the operation. */ - public AffectedObject(DirectoryObjectEvent event) { + public AffectedObject(IdentifiableObjectEvent event) { this.event = event; } From 606c7bd55ceb62bd404e949ef5c8042ff65f0936 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 5 Oct 2022 09:32:08 -0700 Subject: [PATCH 092/289] GUACAMOLE-1224: Improve readability of JavaDoc by shortening text of hyperlinked enum values. --- .../java/org/apache/guacamole/net/event/DirectoryEvent.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java index 5e9116496..468851af1 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/DirectoryEvent.java @@ -91,11 +91,11 @@ public interface DirectoryEvent /** * {@inheritDoc} *

- * Currently, for object creation ({@link Operation#ADD}), retrieval - * ({@link Operation#GET}), modification and ({@link Operation#UPDATE}), + * Currently, for object creation ({@link Operation#ADD ADD}), retrieval + * ({@link Operation#GET GET}), and modification ({@link Operation#UPDATE UPDATE}), * it can be expected that the affected object will be available, however * the caller should verify this regardless. For deletions - * ({@link Operation#REMOVE}), the object can only be made available for + * ({@link Operation#REMOVE REMOVE}), the object can only be made available for * single deletions, and cannot be made available for batch deletions. */ @Override From 36b5842d426d907482c7db27c3bbb91a5daa9de5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 5 Oct 2022 09:41:54 -0700 Subject: [PATCH 093/289] GUACAMOLE-1224: Log user password updates. --- .../guacamole/event/AffectedObject.java | 4 +++ .../guacamole/event/EventLoggingListener.java | 33 +++++++++++++++++-- .../guacamole/rest/user/UserResource.java | 32 +++++++++++++----- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java index b95122bae..c610bf177 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java @@ -94,6 +94,10 @@ public class AffectedObject implements LoggableDetail { // Users case USER: + + if (identifier.equals(event.getAuthenticatedUser().getIdentifier())) + return "their own user account"; + objectType = "user"; break; diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java index 90a929765..b21b8b230 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -24,6 +24,8 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.Identifiable; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.event.ApplicationShutdownEvent; import org.apache.guacamole.net.event.ApplicationStartedEvent; @@ -32,6 +34,7 @@ import org.apache.guacamole.net.event.AuthenticationSuccessEvent; import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent; import org.apache.guacamole.net.event.DirectorySuccessEvent; +import org.apache.guacamole.net.event.IdentifiableObjectEvent; import org.apache.guacamole.net.event.UserSessionInvalidatedEvent; import org.apache.guacamole.net.event.listener.Listener; import org.slf4j.Logger; @@ -48,6 +51,26 @@ public class EventLoggingListener implements Listener { */ private final Logger logger = LoggerFactory.getLogger(EventLoggingListener.class); + /** + * Returns whether the given event affects the password of a User object. + * + * @param event + * The event to check. + * + * @return + * true if a user's password is specifically set or modified by the + * given event, false otherwise. + */ + private boolean isPasswordAffected(IdentifiableObjectEvent event) { + + Identifiable object = event.getObject(); + if (!(object instanceof User)) + return false; + + return ((User) object).getPassword() != null; + + } + /** * Logs that an operation was performed on an object within a Directory * successfully. @@ -65,11 +88,17 @@ public class EventLoggingListener implements Listener { break; case ADD: - logger.info("{} successfully created {}", new RequestingUser(event), new AffectedObject(event)); + if (isPasswordAffected(event)) + logger.info("{} successfully created {}, setting their password", new RequestingUser(event), new AffectedObject(event)); + else + logger.info("{} successfully created {}", new RequestingUser(event), new AffectedObject(event)); break; case UPDATE: - logger.info("{} successfully updated {}", new RequestingUser(event), new AffectedObject(event)); + if (isPasswordAffected(event)) + logger.info("{} successfully updated {}, changing their password", new RequestingUser(event), new AffectedObject(event)); + else + logger.info("{} successfully updated {}", new RequestingUser(event), new AffectedObject(event)); break; case REMOVE: diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java index 9b79fbb48..91600f736 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java @@ -41,6 +41,7 @@ import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.simple.SimpleActivityRecordSet; +import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.rest.directory.DirectoryObjectResource; import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; import org.apache.guacamole.rest.history.UserHistoryResource; @@ -134,13 +135,19 @@ public class UserResource public void updateObject(APIUser modifiedObject) throws GuacamoleException { // A user may not use this endpoint to update their password - User currentUser = getUserContext().self(); - if ( - currentUser.getIdentifier().equals(modifiedObject.getUsername()) - && modifiedObject.getPassword() != null) { - throw new GuacamoleSecurityException( - "Permission denied. The password update endpoint must" - + " be used to change the current user's password."); + try { + User currentUser = getUserContext().self(); + if ( + currentUser.getIdentifier().equals(modifiedObject.getUsername()) + && modifiedObject.getPassword() != null) { + throw new GuacamoleSecurityException( + "Permission denied. The password update endpoint must" + + " be used to change the current user's password."); + } + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent(DirectoryEvent.Operation.UPDATE, e); + throw e; } super.updateObject(modifiedObject); @@ -184,8 +191,15 @@ public class UserResource } // Set password to the newly provided one - user.setPassword(userPasswordUpdate.getNewPassword()); - getDirectory().update(user); + try { + user.setPassword(userPasswordUpdate.getNewPassword()); + getDirectory().update(user); + fireDirectorySuccessEvent(DirectoryEvent.Operation.UPDATE); + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent(DirectoryEvent.Operation.UPDATE, e); + throw e; + } } From f43b1198183b35782471a95c8cf3b3ef6565180c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 5 Oct 2022 10:34:52 -0700 Subject: [PATCH 094/289] GUACAMOLE-1224: Log receipt of authentication requests at trace level. --- .../java/org/apache/guacamole/event/EventLoggingListener.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java index b21b8b230..16bee6ae8 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/EventLoggingListener.java @@ -30,6 +30,7 @@ import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredential import org.apache.guacamole.net.event.ApplicationShutdownEvent; import org.apache.guacamole.net.event.ApplicationStartedEvent; import org.apache.guacamole.net.event.AuthenticationFailureEvent; +import org.apache.guacamole.net.event.AuthenticationRequestReceivedEvent; import org.apache.guacamole.net.event.AuthenticationSuccessEvent; import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent; @@ -227,6 +228,9 @@ public class EventLoggingListener implements Listener { else if (event instanceof UserSessionInvalidatedEvent) logger.info("{} has logged out, or their session has expired or " + "been terminated.", new RequestingUser((UserSessionInvalidatedEvent) event)); + else if (event instanceof AuthenticationRequestReceivedEvent) + logger.trace("Authentication request received from {}", + new RemoteAddress(((AuthenticationRequestReceivedEvent) event).getCredentials())); // Application startup/shutdown else if (event instanceof ApplicationStartedEvent) From 89433ee3c7c3fe12b85ac5de9f2e8669218a2e8f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 5 Oct 2022 10:42:59 -0700 Subject: [PATCH 095/289] GUACAMOLE-1224: Log extension sources of objects affected by REST API. --- .../org/apache/guacamole/event/AffectedObject.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java index c610bf177..c08b4ed03 100644 --- a/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java +++ b/guacamole/src/main/java/org/apache/guacamole/event/AffectedObject.java @@ -55,6 +55,7 @@ public class AffectedObject implements LoggableDetail { Object object = event.getObject(); String identifier = event.getObjectIdentifier(); + String dataSource = event.getAuthenticationProvider().getIdentifier(); String objectType; String name = null; // Not all objects have names @@ -95,8 +96,8 @@ public class AffectedObject implements LoggableDetail { // Users case USER: - if (identifier.equals(event.getAuthenticatedUser().getIdentifier())) - return "their own user account"; + if (identifier != null && identifier.equals(event.getAuthenticatedUser().getIdentifier())) + return "their own user account within \"" + dataSource + "\""; objectType = "user"; break; @@ -116,12 +117,12 @@ public class AffectedObject implements LoggableDetail { // including the name of the object, as well, if available if (identifier != null) { if (name != null) - return objectType + " \"" + identifier + "\" (currently named \"" + name + "\")"; + return objectType + " \"" + identifier + "\" within \"" + dataSource + "\" (currently named \"" + name + "\")"; else - return objectType + " \"" + identifier + "\""; + return objectType + " \"" + identifier + "\" within \"" + dataSource + "\""; } else - return objectType; + return objectType + " within \"" + dataSource + "\""; } From 79f72aad4fc0feece22c6ff0397430607da1ed5c Mon Sep 17 00:00:00 2001 From: Mike Jumper Date: Sun, 13 Nov 2022 12:12:59 -0800 Subject: [PATCH 096/289] GUACAMOLE-1716: Remove usage of AccessController (deprecated in Java 17 and later). --- .../extension/DirectoryClassLoader.java | 43 +-------------- .../apache/guacamole/extension/Extension.java | 2 +- .../extension/ExtensionClassLoader.java | 55 +------------------ .../guacamole/extension/ExtensionModule.java | 2 +- 4 files changed, 4 insertions(+), 98 deletions(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java b/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java index 215c3c00c..83ad9be73 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/DirectoryClassLoader.java @@ -24,9 +24,6 @@ import java.io.FilenameFilter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Collection; import org.apache.guacamole.GuacamoleException; @@ -37,44 +34,6 @@ import org.apache.guacamole.GuacamoleException; */ public class DirectoryClassLoader extends URLClassLoader { - /** - * Returns an instance of DirectoryClassLoader configured to load .jar - * files from the given directory. Calling this function multiple times - * will not affect previously-returned instances of DirectoryClassLoader. - * - * @param dir - * The directory from which .jar files should be read. - * - * @return - * A DirectoryClassLoader instance which loads classes from the .jar - * files in the given directory. - * - * @throws GuacamoleException - * If the given file is not a directory, or the contents of the given - * directory cannot be read. - */ - public static DirectoryClassLoader getInstance(final File dir) - throws GuacamoleException { - - try { - // Attempt to create singleton classloader which loads classes from - // all .jar's in the lib directory defined in guacamole.properties - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - - @Override - public DirectoryClassLoader run() throws GuacamoleException { - return new DirectoryClassLoader(dir); - } - - }); - } - - catch (PrivilegedActionException e) { - throw (GuacamoleException) e.getException(); - } - - } - /** * Returns all .jar files within the given directory as an array of URLs. * @@ -142,7 +101,7 @@ public class DirectoryClassLoader extends URLClassLoader { * directory cannot be read. */ - private DirectoryClassLoader(File dir) throws GuacamoleException { + public DirectoryClassLoader(File dir) throws GuacamoleException { super(getJarURLs(dir), DirectoryClassLoader.class.getClassLoader()); } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java index f6cf80df2..be67a384b 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java @@ -397,7 +397,7 @@ public class Extension { } // Create isolated classloader for this extension - classLoader = ExtensionClassLoader.getInstance(file, temporaryFiles, parent); + classLoader = new ExtensionClassLoader(file, temporaryFiles, parent); } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java index 03faf53cb..6473aa230 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionClassLoader.java @@ -29,9 +29,6 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; @@ -74,56 +71,6 @@ public class ExtensionClassLoader extends URLClassLoader { */ private final ClassLoader parent; - /** - * Returns an instance of ExtensionClassLoader configured to load classes - * from the given extension .jar. If a necessary class cannot be found - * within the .jar, the given parent ClassLoader is used. Calling this - * function multiple times will not affect previously-returned instances of - * ExtensionClassLoader. - * - * @param extension - * The extension .jar file from which classes should be loaded. - * - * @param temporaryFiles - * A modifiable List that should be populated with all temporary files - * created for the given extension. These files should be deleted on - * application shutdown in reverse order. - * - * @param parent - * The ClassLoader to use if class resolution through the extension - * .jar fails. - * - * @return - * A ExtensionClassLoader instance which loads classes from the - * given extension .jar file. - * - * @throws GuacamoleException - * If the given file is not actually a file, or the contents of the - * file cannot be read. - */ - public static ExtensionClassLoader getInstance(final File extension, - final List temporaryFiles, final ClassLoader parent) - throws GuacamoleException { - - try { - // Attempt to create classloader which loads classes from the given - // .jar file - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - - @Override - public ExtensionClassLoader run() throws GuacamoleException { - return new ExtensionClassLoader(extension, temporaryFiles, parent); - } - - }); - } - - catch (PrivilegedActionException e) { - throw (GuacamoleException) e.getException(); - } - - } - /** * Returns the URL that refers to the given file. If the given file refers * to a directory, an exception is thrown. @@ -320,7 +267,7 @@ public class ExtensionClassLoader extends URLClassLoader { * If the given file is not actually a file, or the contents of the * file cannot be read. */ - private ExtensionClassLoader(File extension, List temporaryFiles, + public ExtensionClassLoader(File extension, List temporaryFiles, ClassLoader parent) throws GuacamoleException { super(getExtensionURLs(extension, temporaryFiles), null); this.parent = parent; diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java index e5fbe2e57..d13b9d0ad 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java @@ -182,7 +182,7 @@ public class ExtensionModule extends ServletModule { return ExtensionModule.class.getClassLoader(); // Return classloader which loads classes from all .jars within the lib directory - return DirectoryClassLoader.getInstance(libDir); + return new DirectoryClassLoader(libDir); } From c82ca370e209ba836ff77df237310e7ea79d9c49 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 22 Nov 2022 20:32:18 +0000 Subject: [PATCH 097/289] GUACAMOLE-1656: Fix user preferences permissioning. --- .../vault/ksm/conf/KsmAttributeService.java | 20 +++++++++++-------- .../ksm/conf/KsmConfigurationService.java | 8 +++++--- .../vault/ksm/user/KsmDirectoryService.java | 1 - .../directives/guacSettingsPreferences.js | 17 ++++++++-------- .../templates/settingsPreferences.html | 6 +++--- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java index c1d3b95d5..8b3c5d938 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmAttributeService.java @@ -145,11 +145,6 @@ public class KsmAttributeService implements VaultAttributeService { @Override public Collection getUserAttributes() { - return KSM_ATTRIBUTES; - } - - @Override - public Collection getUserPreferenceAttributes() { try { @@ -161,15 +156,24 @@ public class KsmAttributeService implements VaultAttributeService { catch (GuacamoleException e) { logger.warn( - "Unable to determine if user preference attributes " + "Unable to determine if KSM user attributes " + "should be exposed due to config parsing error: {}.", e.getMessage()); logger.debug( - "Config parsing error prevented checking user preference configuration", + "Config parsing error prevented checking user attribute configuration", e); - // If the configuration can't be parsed, default to not exposing the attribute + // If the configuration can't be parsed, default to not exposing the attributes return Collections.emptyList(); } + + } + + @Override + public Collection getUserPreferenceAttributes() { + + // KSM-specific user preference attributes have the same semantics as + // user attributes + return getUserAttributes(); } /** diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java index 9d2e4565f..5571ef9df 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java @@ -150,11 +150,13 @@ public class KsmConfigurationService extends VaultConfigurationService { } /** - * Return whether users should be able to provide their own KSM configs. + * Return whether user-level KSM configs should be enabled. If this + * flag is set to true, users can edit their own KSM configs, as can + * admins. If set to false, no existing user-specific KSM configuration + * will be exposed through the UI or used when looking up secrets. * * @return - * true if users should be able to provide their own KSM configs, - * false otherwise. + * true if user-specific KSM configuration is enabled, false otherwise. * * @throws GuacamoleException * If the value specified within guacamole.properties cannot be diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java index 5203a06dd..18934c99e 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/user/KsmDirectoryService.java @@ -23,7 +23,6 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.DecoratingDirectory; -import org.apache.guacamole.net.auth.DelegatingDirectory; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.vault.ksm.conf.KsmAttributeService; diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js index a3f0fd2e5..005d2398b 100644 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js +++ b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js @@ -143,12 +143,13 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe $scope.newPasswordMatch = null; /** - * Whether the current user can change their own password, or null - * if this is not yet known. + * Whether the current user can edit themselves - i.e. update their + * password or change user preference attributes, or null if this + * is not yet known. * * @type Boolean */ - $scope.canChangePassword = null; + $scope.canUpdateSelf = null; /** * Update the current user's password to the password currently set within @@ -206,13 +207,13 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe permissionService.getEffectivePermissions(dataSource, username) .then(function permissionsRetrieved(permissions) { - // Add action for changing password if permission is granted - $scope.canChangePassword = PermissionSet.hasUserPermission(permissions, + // Add action for updaing password or user preferences if permission is granted + $scope.canUpdateSelf = PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, username); }) ['catch'](requestService.createErrorCallback(function permissionsFailed(error) { - $scope.canChangePassword = false; + $scope.canUpdateSelf = false; })); /** @@ -224,8 +225,8 @@ angular.module('settings').directive('guacSettingsPreferences', [function guacSe */ $scope.isLoaded = function isLoaded() { - return $scope.canChangePassword !== null - && $scope.languages !== null; + return $scope.canUpdateSelf !== null + && $scope.languages !== null; }; diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html index 6ecf6e0e7..7c154df89 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html @@ -7,8 +7,8 @@

-

{{'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_PASSWORD' | translate}}

-
+

{{'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_PASSWORD' | translate}}

+

{{'SETTINGS_PREFERENCES.HELP_UPDATE_PASSWORD' | translate}}

@@ -90,7 +90,7 @@
-
+
From 69334b05e21813b57acadd9aec2478574366b440 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 29 Nov 2022 19:22:11 +0000 Subject: [PATCH 098/289] GUACAMOLE-1721: Store only auth token from auth response in localStorage. --- .../app/auth/service/authenticationService.js | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index 199d6cb2e..ce44b184c 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -61,18 +61,21 @@ angular.module('auth').factory('authenticationService', ['$injector', var cachedResult = null; /** - * The unique identifier of the local storage key which stores the result - * of the last authentication attempt. + * The unique identifier of the local storage key which stores the latest + * authentication token. * * @type String */ - var AUTH_STORAGE_KEY = 'GUAC_AUTH'; + var AUTH_TOKEN_STORAGE_KEY = 'GUAC_AUTH_TOKEN'; /** - * Retrieves the last successful authentication result. If the user has not + * Retrieves the authentication result cached in memory. If the user has not * yet authenticated, the user has logged out, or the last authentication * attempt failed, null is returned. * + * NOTE: setAuthenticationResult() will be called upon page load, so the + * cache should always be populated after the page has successfully loaded. + * * @returns {AuthenticationResult} * The last successful authentication result, or null if the user is not * currently authenticated. @@ -84,12 +87,7 @@ angular.module('auth').factory('authenticationService', ['$injector', return cachedResult; // Return explicit null if no auth data is currently stored - var data = localStorageService.getItem(AUTH_STORAGE_KEY); - if (!data) - return null; - - // Update cache and return retrieved auth result - return (cachedResult = new AuthenticationResult(data)); + return null; }; @@ -103,21 +101,28 @@ angular.module('auth').factory('authenticationService', ['$injector', */ var setAuthenticationResult = function setAuthenticationResult(data) { - // Clear the currently-stored result if the last attempt failed + // Clear the currently-stored result and auth token if the last + // attempt failed if (!data) { cachedResult = null; - localStorageService.removeItem(AUTH_STORAGE_KEY); + localStorageService.removeItem(AUTH_TOKEN_STORAGE_KEY); } - // Otherwise store the authentication attempt directly + // Otherwise, store the authentication attempt directly. + // Note that only the auth token is stored in persistent local storage. + // To re-obtain an autentication result upon a fresh page load, + // reauthenticate with the persistent token, which can be obtained by + // calling getCurrentToken(). else { // Always store in cache cachedResult = data; - // Persist result past tab/window closure ONLY if not anonymous + // Persist only the auth token past tab/window closure, and only + // if not anonymous if (data.username !== AuthenticationResult.ANONYMOUS_USERNAME) - localStorageService.setItem(AUTH_STORAGE_KEY, data); + localStorageService.setItem( + AUTH_TOKEN_STORAGE_KEY, data.authToken); } @@ -363,13 +368,13 @@ angular.module('auth').factory('authenticationService', ['$injector', */ service.getCurrentToken = function getCurrentToken() { - // Return auth token, if available + // Return cached auth token, if available var authData = getAuthenticationResult(); if (authData) return authData.authToken; - // No auth data present - return null; + // Fall back to the value from local storage if not found in cache + return localStorageService.getItem(AUTH_TOKEN_STORAGE_KEY); }; From b9044caf65f8d56a77dd994823a50eb7dfb48cef Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 29 Nov 2022 20:58:51 +0000 Subject: [PATCH 099/289] GUACAMOLE-1772: Allow user configuration of KSM API call timeout. --- .../ksm/conf/KsmConfigurationService.java | 32 +++++++++++++++++++ .../guacamole/vault/ksm/secret/KsmClient.java | 31 +++++++++++------- .../vault/ksm/secret/KsmClientFactory.java | 9 ++++-- .../vault/ksm/secret/KsmSecretService.java | 3 +- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java index 5571ef9df..197b979e2 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/conf/KsmConfigurationService.java @@ -28,6 +28,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.BooleanGuacamoleProperty; +import org.apache.guacamole.properties.LongGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; import org.apache.guacamole.vault.conf.VaultConfigurationService; @@ -120,6 +121,17 @@ public class KsmConfigurationService extends VaultConfigurationService { } }; + /** + * The minimum number of milliseconds between KSM API calls. + */ + private static final LongGuacamoleProperty KSM_API_CALL_INTERVAL = new LongGuacamoleProperty() { + + @Override + public String getName() { + return "ksm-api-call-interval"; + } + }; + /** * Creates a new KsmConfigurationService which reads the configuration * from "ksm-token-mapping.yml" and properties from @@ -176,6 +188,20 @@ public class KsmConfigurationService extends VaultConfigurationService { return environment.getProperty(MATCH_USER_DOMAINS, false); } + /** + * Return the minimum number of milliseconds between KSM API calls. If not + * otherwise configured, this value will be 10 seconds. + * + * @return + * The minimum number of milliseconds between KSM API calls. + * + * @throws GuacamoleException + * If the value specified within guacamole.properties cannot be + * parsed or does not exist. + */ + public long getKsmApiInterval() throws GuacamoleException { + return environment.getProperty(KSM_API_CALL_INTERVAL, 10000L); + } /** * Return the globally-defined base-64-encoded JSON KSM configuration blob @@ -189,7 +215,12 @@ public class KsmConfigurationService extends VaultConfigurationService { * If the value specified within guacamole.properties cannot be * parsed or does not exist. */ + @Nonnull + @SuppressWarnings("null") public String getKsmConfig() throws GuacamoleException { + + // This will always return a non-null value; an exception would be + // thrown if the required value is not set return environment.getRequiredProperty(KSM_CONFIG); } @@ -235,6 +266,7 @@ public class KsmConfigurationService extends VaultConfigurationService { * @throws GuacamoleException * If an invalid ksmConfig parameter is provided. */ + @Nonnull public SecretsManagerOptions getSecretsManagerOptions(@Nonnull String ksmConfig) throws GuacamoleException { return new SecretsManagerOptions( diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java index 3aa436f69..1dbcd73a8 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClient.java @@ -95,12 +95,6 @@ public class KsmClient { */ private static final Pattern KEEPER_FILE_NOTATION = Pattern.compile("^(keeper://)?[^/]*/file/.+"); - /** - * The maximum amount of time that an entry will be stored in the cache - * before being refreshed, in milliseconds. - */ - private static final long CACHE_INTERVAL = 5000; - /** * The KSM configuration associated with this client instance. */ @@ -115,6 +109,13 @@ public class KsmClient { */ private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); + /** + * The maximum amount of time that an entry will be stored in the cache + * before being refreshed, in milliseconds. This is also the shortest + * possible interval between API calls to KSM. + */ + private final long cacheInterval; + /** * The timestamp that the cache was last refreshed, in milliseconds, as * returned by System.currentTimeMillis(). This value is automatically @@ -215,19 +216,27 @@ public class KsmClient { private final Set cachedAmbiguousDomains = new HashSet<>(); /** - * Create a new KSM client based around the provided KSM configuration. + * Create a new KSM client based around the provided KSM configuration and + * API timeout setting. * * @param ksmConfig * The KSM configuration to use when retrieving properties from KSM. + * + * @param apiInterval + * The minimum number of milliseconds that must elapse between KSM API + * calls. */ @AssistedInject - public KsmClient(@Assisted SecretsManagerOptions ksmConfig) { + public KsmClient( + @Assisted SecretsManagerOptions ksmConfig, + @Assisted long apiInterval) { this.ksmConfig = ksmConfig; + this.cacheInterval = apiInterval; } /** * Validates that all cached data is current with respect to - * {@link #CACHE_INTERVAL}, refreshing data from the server as needed. + * {@link #cacheInterval}, refreshing data from the server as needed. * * @throws GuacamoleException * If an error occurs preventing the cached data from being refreshed. @@ -240,7 +249,7 @@ public class KsmClient { // continuing cacheLock.readLock().lock(); try { - if (currentTime - cacheTimestamp < CACHE_INTERVAL) + if (currentTime - cacheTimestamp < cacheInterval) return; } finally { @@ -252,7 +261,7 @@ public class KsmClient { // Cache may have been updated since the read-only check. Re-verify // that the cache has expired before continuing with a full refresh - if (currentTime - cacheTimestamp < CACHE_INTERVAL) + if (currentTime - cacheTimestamp < cacheInterval) return; // Attempt to pull all records first, allowing that operation to diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java index f8220c16f..d97711893 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmClientFactory.java @@ -30,16 +30,21 @@ public interface KsmClientFactory { /** * Returns a new instance of a KsmClient instance associated with - * the provided KSM configuration options. + * the provided KSM configuration options and API interval. * * @param ksmConfigOptions * The KSM config options to use when constructing the KsmClient * object. * + * @param apiInterval + * The minimum number of milliseconds that must elapse between KSM API + * calls. + * * @return * A new KsmClient instance associated with the provided KSM config * options. */ - KsmClient create(@Nonnull SecretsManagerOptions ksmConfigOptions); + KsmClient create( + @Nonnull SecretsManagerOptions ksmConfigOptions, long apiInterval); } diff --git a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java index f4d3c0626..717a125e3 100644 --- a/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java +++ b/extensions/guacamole-vault/modules/guacamole-vault-ksm/src/main/java/org/apache/guacamole/vault/ksm/secret/KsmSecretService.java @@ -124,7 +124,7 @@ public class KsmSecretService implements VaultSecretService { // Create and store a new KSM client instance for the provided KSM config blob SecretsManagerOptions options = confService.getSecretsManagerOptions(ksmConfig); - ksmClient = ksmClientFactory.create(options); + ksmClient = ksmClientFactory.create(options, confService.getKsmApiInterval()); KsmClient prevClient = ksmClientMap.putIfAbsent(ksmConfig, ksmClient); // If the client was already set before this thread got there, use the existing one @@ -274,6 +274,7 @@ public class KsmSecretService implements VaultSecretService { * no KSM config is found in the connection group tree, and the value is also not * defined in the config file. */ + @Nonnull private String getConnectionGroupKsmConfig( UserContext userContext, Connectable connectable) throws GuacamoleException { From 18d971a8378c1c4fbdae4b0a9f7e2bec3fa14750 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 1 Dec 2022 00:22:02 +0000 Subject: [PATCH 100/289] GUACAMOLE-1723: Enforce access time restrictions for logged in users. --- .../modules/guacamole-auth-jdbc-base/pom.xml | 6 + .../JDBCAuthenticationProviderModule.java | 13 +- .../guacamole/auth/jdbc/JDBCEnvironment.java | 15 ++ .../ActiveConnectionDirectory.java | 6 + .../auth/jdbc/base/RestrictedObject.java | 47 +++++ .../jdbc/connection/ConnectionDirectory.java | 6 + .../ConnectionGroupDirectory.java | 6 + .../SharingProfileDirectory.java | 6 + .../AbstractGuacamoleTunnelService.java | 71 +++++-- .../AccessEnforcingDelegatingTunnel.java | 196 ++++++++++++++++++ ...ccessEnforcingDelegatingTunnelFactory.java | 54 +++++ .../jdbc/tunnel/ActiveConnectionRecord.java | 7 + .../auth/jdbc/user/UserDirectory.java | 6 + .../jdbc/usergroup/UserGroupDirectory.java | 6 + .../auth/mysql/conf/MySQLEnvironment.java | 9 + .../mysql/conf/MySQLGuacamoleProperties.java | 13 ++ .../conf/PostgreSQLEnvironment.java | 9 + .../conf/PostgreSQLGuacamoleProperties.java | 13 ++ .../sqlserver/conf/SQLServerEnvironment.java | 9 + .../conf/SQLServerGuacamoleProperties.java | 13 ++ 20 files changed, 497 insertions(+), 14 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml index 9c99593bf..ac9856240 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml @@ -94,6 +94,12 @@ guice + + + com.google.inject.extensions + guice-assistedinject + + com.google.guava diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java index 5ae0ea53f..e47ccf259 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java @@ -20,6 +20,8 @@ package org.apache.guacamole.auth.jdbc; import com.google.inject.Scopes; +import com.google.inject.assistedinject.FactoryModuleBuilder; + import javax.sql.DataSource; import org.apache.guacamole.auth.jdbc.user.ModeledUserContext; import org.apache.guacamole.auth.jdbc.connectiongroup.RootConnectionGroup; @@ -38,6 +40,8 @@ import org.apache.guacamole.auth.jdbc.permission.SystemPermissionMapper; import org.apache.guacamole.auth.jdbc.user.UserMapper; import org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupService; import org.apache.guacamole.auth.jdbc.connection.ConnectionService; +import org.apache.guacamole.auth.jdbc.tunnel.AccessEnforcingDelegatingTunnel; +import org.apache.guacamole.auth.jdbc.tunnel.AccessEnforcingDelegatingTunnelFactory; import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.apache.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.apache.guacamole.auth.jdbc.security.SHA256PasswordEncryptionService; @@ -196,7 +200,14 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule { bind(UserGroupPermissionService.class); bind(UserPermissionService.class); bind(UserService.class); - + + // Bind Factories + install(new FactoryModuleBuilder() + .implement( + AccessEnforcingDelegatingTunnel.class, + AccessEnforcingDelegatingTunnel.class) + .build(AccessEnforcingDelegatingTunnelFactory.class)); + } } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java index 763793d62..778d5b29f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java @@ -227,4 +227,19 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { */ public abstract boolean trackExternalConnectionHistory() throws GuacamoleException; + /** + * Returns a boolean value representing whether access time windows should + * be enforced for active connections - i.e. whether a currently-connected + * user should be disconnected upon the closure of an access window, or + * when the user is disabled. + * + * @return + * True if a connected user should be disconnected upon an access time + * window closing, or the user being disabled. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public abstract boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java index b0d6324aa..b9a149933 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java @@ -44,34 +44,40 @@ public class ActiveConnectionDirectory extends RestrictedObject @Override public ActiveConnection get(String identifier) throws GuacamoleException { + validateUserAccess(); return activeConnectionService.retrieveObject(getCurrentUser(), identifier); } @Override public Collection getAll(Collection identifiers) throws GuacamoleException { + validateUserAccess(); Collection objects = activeConnectionService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @Override public Set getIdentifiers() throws GuacamoleException { + validateUserAccess(); return activeConnectionService.getIdentifiers(getCurrentUser()); } @Override public void add(ActiveConnection object) throws GuacamoleException { + validateUserAccess(); activeConnectionService.createObject(getCurrentUser(), object); } @Override public void update(ActiveConnection object) throws GuacamoleException { + validateUserAccess(); TrackedActiveConnection connection = (TrackedActiveConnection) object; activeConnectionService.updateObject(getCurrentUser(), connection); } @Override public void remove(String identifier) throws GuacamoleException { + validateUserAccess(); activeConnectionService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java index 66979d487..461ce5804 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java @@ -19,7 +19,13 @@ package org.apache.guacamole.auth.jdbc.base; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleUnauthorizedException; +import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser; +import org.apache.guacamole.auth.jdbc.user.ModeledUser; + +import com.google.inject.Inject; /** * Common base class for objects that are associated with the users that @@ -33,6 +39,12 @@ public abstract class RestrictedObject { */ private ModeledAuthenticatedUser currentUser; + /** + * The environment of the Guacamole server. + */ + @Inject + private JDBCEnvironment environment; + /** * Initializes this object, associating it with the current authenticated * user and populating it with data from the given model object @@ -68,4 +80,39 @@ public abstract class RestrictedObject { this.currentUser = currentUser; } + /** + * Validate that the current user is within a valid access time window + * and not disabled. If the user account is disabled or not within a + * valid access window, a GuacamoleUnauthorizedException will be thrown. + * + * This method can be called by RestrictedObject implementations before + * any operation that's specific to the current logged in user, to make + * sure that their access is still valid and enabled. + * + * If accessWindowCheckEnabled is set to false, the check will be skipped, + * and GuacamoleUnauthorizedException will never be thrown. + * + * @throws GuacamoleException + * If the user is outside of a valid access window, the user is + * disabled, or an error occurs while trying to determine access time + * restriction configuration. + */ + protected void validateUserAccess() throws GuacamoleException { + + // If access windows shouldn't be checked for active sessions, skip + // this check entirely + if (!environment.enforceAccessWindowsForActiveSessions()) + return; + + // If the user is outside of a valid access time window or disabled, + // throw an exception to immediately log them out + ModeledUser modeledUser = getCurrentUser().getUser(); + if ( + !modeledUser.isAccountAccessible() + || !modeledUser.isAccountValid() + || modeledUser.isDisabled() + ) + throw new GuacamoleUnauthorizedException("Permission Denied."); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 52a127df4..54ca79033 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -45,12 +45,14 @@ public class ConnectionDirectory extends RestrictedObject @Override public Connection get(String identifier) throws GuacamoleException { + validateUserAccess(); return connectionService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { + validateUserAccess(); Collection objects = connectionService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -58,18 +60,21 @@ public class ConnectionDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { + validateUserAccess(); return connectionService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(Connection object) throws GuacamoleException { + validateUserAccess(); connectionService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(Connection object) throws GuacamoleException { + validateUserAccess(); ModeledConnection connection = (ModeledConnection) object; connectionService.updateObject(getCurrentUser(), connection); } @@ -77,6 +82,7 @@ public class ConnectionDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { + validateUserAccess(); connectionService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java index 9f3930597..030432793 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java @@ -45,12 +45,14 @@ public class ConnectionGroupDirectory extends RestrictedObject @Override public ConnectionGroup get(String identifier) throws GuacamoleException { + validateUserAccess(); return connectionGroupService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { + validateUserAccess(); Collection objects = connectionGroupService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -58,18 +60,21 @@ public class ConnectionGroupDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { + validateUserAccess(); return connectionGroupService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(ConnectionGroup object) throws GuacamoleException { + validateUserAccess(); connectionGroupService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(ConnectionGroup object) throws GuacamoleException { + validateUserAccess(); ModeledConnectionGroup connectionGroup = (ModeledConnectionGroup) object; connectionGroupService.updateObject(getCurrentUser(), connectionGroup); } @@ -77,6 +82,7 @@ public class ConnectionGroupDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { + validateUserAccess(); connectionGroupService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java index 632557052..57ccb6d63 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java @@ -44,12 +44,14 @@ public class SharingProfileDirectory extends RestrictedObject @Override public SharingProfile get(String identifier) throws GuacamoleException { + validateUserAccess(); return sharingProfileService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { + validateUserAccess(); return Collections.unmodifiableCollection( sharingProfileService.retrieveObjects(getCurrentUser(), identifiers) ); @@ -58,18 +60,21 @@ public class SharingProfileDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { + validateUserAccess(); return sharingProfileService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(SharingProfile object) throws GuacamoleException { + validateUserAccess(); sharingProfileService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(SharingProfile object) throws GuacamoleException { + validateUserAccess(); ModeledSharingProfile sharingProfile = (ModeledSharingProfile) object; sharingProfileService.updateObject(getCurrentUser(), sharingProfile); } @@ -77,6 +82,7 @@ public class SharingProfileDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { + validateUserAccess(); sharingProfileService.deleteObject(getCurrentUser(), identifier); } 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 8c16363a6..f26b8ea13 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 @@ -46,6 +46,7 @@ import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleUpstreamException; +import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.connection.ConnectionMapper; import org.apache.guacamole.net.GuacamoleSocket; import org.apache.guacamole.net.GuacamoleTunnel; @@ -63,6 +64,7 @@ import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile; import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterMapper; import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterModel; import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser; +import org.apache.guacamole.auth.jdbc.user.UserService; import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration; import org.apache.guacamole.protocol.FailoverGuacamoleSocket; import org.slf4j.Logger; @@ -117,6 +119,24 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS @Inject private SharedConnectionMap connectionMap; + /** + * Service for fetching users. + */ + @Inject + private UserService userService; + + /** + * The environment of the Guacamole server. + */ + @Inject + private JDBCEnvironment environment; + + /** + * Provider for creating AccessEnforcingDelegatingTunnel instances. + */ + @Inject + private AccessEnforcingDelegatingTunnelFactory accessEnforcingTunnelFactory; + /** * All active connections through the tunnel having a given UUID. */ @@ -385,13 +405,21 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS * connection, which MUST already be acquired via acquire(). The given * client information will be passed to guacd when the connection is * established. - * + * * The connection will be automatically released when it closes, or if it * fails to establish entirely. * + * If user access window restrictions are enabled for active sessions, + * they will be automatically enforced for any valid database users as the + * tunnel is used. + * * @param activeConnection * The active connection record of the connection in use. * + * @param user + * The user record in the database for the user connecting to the + * connection, if any. + * * @param info * Information describing the Guacamole client connecting to the given * connection. @@ -412,8 +440,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS * If an error occurs while the connection is being established, or * while connection configuration information is being retrieved. */ - private GuacamoleTunnel assignGuacamoleTunnel(ActiveConnectionRecord activeConnection, - GuacamoleClientInformation info, Map tokens, + private GuacamoleTunnel assignGuacamoleTunnel( + ActiveConnectionRecord activeConnection, + ModeledAuthenticatedUser user, GuacamoleClientInformation info, + Map tokens, boolean interceptErrors) throws GuacamoleException { // Record new active connection @@ -476,12 +506,21 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS getUnconfiguredGuacamoleSocket(connection.getGuacamoleProxyConfiguration(), cleanupTask), config, info); - // Assign and return new tunnel + // Assign new tunnel + GuacamoleTunnel tunnel; if (interceptErrors) - return activeConnection.assignGuacamoleTunnel(new FailoverGuacamoleSocket(socket), socket.getConnectionID()); + tunnel = activeConnection.assignGuacamoleTunnel(new FailoverGuacamoleSocket(socket), socket.getConnectionID()); else - return activeConnection.assignGuacamoleTunnel(socket, socket.getConnectionID()); - + tunnel = activeConnection.assignGuacamoleTunnel(socket, socket.getConnectionID()); + + // If there is an associated database user, and access window + // restrictions are enabled for active sessions, wrap the tunnel + // in order to check user login validity on every instruction read + if (user != null && environment.enforceAccessWindowsForActiveSessions()) { + tunnel = accessEnforcingTunnelFactory.create(tunnel, user); + } + + return tunnel; } // Execute cleanup if socket could not be created @@ -631,8 +670,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS acquire(user, Collections.singletonList(connection), true); // Connect only if the connection was successfully acquired - ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord(connectionMap, user, connection); - return assignGuacamoleTunnel(connectionRecord, info, tokens, false); + ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord( + connectionMap, user, connection); + return assignGuacamoleTunnel( + connectionRecord, user, info, tokens, false); } @@ -677,9 +718,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS try { // Connect to acquired child - ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord(connectionMap, user, connectionGroup, connection); - GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, - info, tokens, connections.size() > 1); + ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord( + connectionMap, user, connectionGroup, connection); + GuacamoleTunnel tunnel = assignGuacamoleTunnel( + connectionRecord, user, info, tokens, connections.size() > 1); // If session affinity is enabled, prefer this connection going forward if (connectionGroup.isSessionAffinityEnabled()) @@ -736,7 +778,10 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS user, definition.getActiveConnection(), definition.getSharingProfile()); // Connect to shared connection described by the created record - GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, info, tokens, false); + GuacamoleTunnel tunnel = assignGuacamoleTunnel( + connectionRecord, userService.retrieveAuthenticatedUser( + user.getAuthenticationProvider(), user.getCredentials()), + info, tokens, false); // Register tunnel, such that it is closed when the // SharedConnectionDefinition is invalidated diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java new file mode 100644 index 000000000..5ee08330c --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.jdbc.tunnel; + +import java.util.concurrent.atomic.AtomicReference; + +import javax.annotation.Nonnull; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleUnauthorizedException; +import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser; +import org.apache.guacamole.auth.jdbc.user.ModeledUser; +import org.apache.guacamole.auth.jdbc.user.UserService; +import org.apache.guacamole.io.GuacamoleReader; +import org.apache.guacamole.net.DelegatingGuacamoleTunnel; +import org.apache.guacamole.net.GuacamoleTunnel; +import org.apache.guacamole.protocol.FilteredGuacamoleReader; +import org.apache.guacamole.protocol.GuacamoleFilter; +import org.apache.guacamole.protocol.GuacamoleInstruction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; + +/** + * A tunnel implementation that enforces access window restriction for the + * provided ModeledUser, throwing a GuacamoleUnauthorizedException if the + * user's configured access window has closed, or if the user has become + * disabled. All other tunnel implementation is delegated to the underlying + * tunnel object. + */ +public class AccessEnforcingDelegatingTunnel extends DelegatingGuacamoleTunnel { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger( + AccessEnforcingDelegatingTunnel.class); + + /** + * The number of milliseconds between subsequent refreshes of the user + * from the DB. + */ + private static final long USER_MODEL_REFRESH_INTERVAL = 10000; + + /** + * The user who's access window restrictions should be applied for the + * wrapped tunnel. + */ + private final AtomicReference user; + + /** + * A thread that will continously refresh the user + */ + private final Thread userRefreshThread; + + /** + * A service to use for refreshing the user from the DB. + */ + @Inject + private UserService userService; + + /** + * Create a new tunnel that will enforce the access window restrictions of + * the provided user, during usage of the provided tunnel. + * + * @param tunnel + * The tunnel to delegate to. + * + * @param modeledAuthenticatedUser + * The user whose access restrictions should be applied. + * + */ + @AssistedInject + public AccessEnforcingDelegatingTunnel( + @Nonnull @Assisted GuacamoleTunnel tunnel, + @Nonnull @Assisted ModeledAuthenticatedUser modeledAuthenticatedUser) { + + super(tunnel); + this.user = new AtomicReference<>(modeledAuthenticatedUser.getUser()); + + this.userRefreshThread = new Thread(() -> { + while (true) { + + try { + + // Fetch an up-to-date user record from the DB to ensure + // that any access restrictions modified while this tunnel + // is open will be taken into account + this.user.set(userService.retrieveUser( + modeledAuthenticatedUser.getAuthenticationProvider(), + modeledAuthenticatedUser)); + } + + // If an error occurs while trying to fetch the updated user, + // log the warning / exception and stop the refresh thread + catch (GuacamoleException e) { + + logger.warn( + "Aborting user refresh thread due to error: {}", + e.getMessage()); + logger.debug( + "Exception caught while attempting to refresh user.", e); + + return; + } + + try { + + // Wait a bit before refreshing the user record again + Thread.sleep(USER_MODEL_REFRESH_INTERVAL); + } + + // If interrupted by the tunnel, exit immediately + catch (InterruptedException e) { + return; + } + } + }); + } + + @Override + public GuacamoleReader acquireReader() { + + // Start periodically refreshing the user record + userRefreshThread.start(); + + // Filter received instructions, checking if the user's login + // is still valid for each one. If the login is invalid, + // log them out immediately and close the tunnel. + return new FilteredGuacamoleReader( + super.acquireReader(), + new GuacamoleFilter() { + + @Override + public GuacamoleInstruction filter( + GuacamoleInstruction instruction) throws GuacamoleException { + + // The user record, at most USER_MODEL_REFRESH_INTERVAL + // milliseconds old + ModeledUser modeledUser = user.get(); + + // If the user is outside of a valid access time window, + // or disabled, throw an exception to immediately log them out + if ( + !modeledUser.isAccountAccessible() + || !modeledUser.isAccountValid() + || modeledUser.isDisabled() + ) { + throw new GuacamoleUnauthorizedException("Permission Denied."); + } + + return instruction; + } + } + ); + } + + @Override + public void releaseReader() { + + // Interrupt the refresh thread; it will clean itself up + userRefreshThread.interrupt(); + + super.releaseReader(); + } + + @Override + public void close() throws GuacamoleException { + + // Interrupt the refresh thread; it will clean itself up + userRefreshThread.interrupt(); + + super.close(); + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java new file mode 100644 index 000000000..0c6350766 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.jdbc.tunnel; + +import javax.annotation.Nonnull; + +import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser; +import org.apache.guacamole.net.GuacamoleTunnel; + +/** + * A factory for creating AccessEnforcingDelegatingTunnel instances. + */ +public interface AccessEnforcingDelegatingTunnelFactory { + + /** + * Create and return a new AccessEnforcingDelegatingTunnel wrapping the + * provided tunnel and enforcing the user restrictions associated with the + * provided user. + * + * @param tunnel + * The tunnel to delegate to. + * + * @param user + * The user whose access window restrictions should be applied for the + * wrapped tunel. + * + * @return + * A new AccessEnforcingDelegatingTunnel wrapping the provided tunnel + * and enforcing the user restrictions associated with the provided + * user. + * + */ + public AccessEnforcingDelegatingTunnel create( + @Nonnull GuacamoleTunnel tunnel, + @Nonnull ModeledAuthenticatedUser user); + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java index c3150ca97..df25159a9 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java @@ -21,6 +21,9 @@ package org.apache.guacamole.auth.jdbc.tunnel; import java.util.Date; import java.util.UUID; + +import javax.annotation.Nonnull; + import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel; import org.apache.guacamole.auth.jdbc.connection.ModeledConnection; import org.apache.guacamole.auth.jdbc.connection.ModeledConnectionRecord; @@ -377,6 +380,8 @@ public class ActiveConnectionRecord extends ModeledConnectionRecord { * @return * The newly-created tunnel associated with this connection record. */ + @Nonnull + @SuppressWarnings("null") public GuacamoleTunnel assignGuacamoleTunnel(final GuacamoleSocket socket, String connectionID) { @@ -400,6 +405,8 @@ public class ActiveConnectionRecord extends ModeledConnectionRecord { this.connectionID = connectionID; // Return newly-created tunnel + // NOTE: Guaranteed to be non-null since it was just set in this + // method, and there is no functionality to set it to null return this.tunnel; } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java index dffd8e2ec..db8bea12f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java @@ -45,12 +45,14 @@ public class UserDirectory extends RestrictedObject @Override public User get(String identifier) throws GuacamoleException { + validateUserAccess(); return userService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { + validateUserAccess(); Collection objects = userService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -58,18 +60,21 @@ public class UserDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { + validateUserAccess(); return userService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(User object) throws GuacamoleException { + validateUserAccess(); userService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(User object) throws GuacamoleException { + validateUserAccess(); ModeledUser user = (ModeledUser) object; userService.updateObject(getCurrentUser(), user); } @@ -77,6 +82,7 @@ public class UserDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { + validateUserAccess(); userService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java index 911b8521f..f61af0bea 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java @@ -44,12 +44,14 @@ public class UserGroupDirectory extends RestrictedObject @Override public UserGroup get(String identifier) throws GuacamoleException { + validateUserAccess(); return userGroupService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { + validateUserAccess(); Collection objects = userGroupService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -57,18 +59,21 @@ public class UserGroupDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { + validateUserAccess(); return userGroupService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(UserGroup object) throws GuacamoleException { + validateUserAccess(); userGroupService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(UserGroup object) throws GuacamoleException { + validateUserAccess(); ModeledUserGroup group = (ModeledUserGroup) object; userGroupService.updateObject(getCurrentUser(), group); } @@ -76,6 +81,7 @@ public class UserGroupDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { + validateUserAccess(); userGroupService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java index d8344ec81..9ac722624 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java @@ -412,4 +412,13 @@ public class MySQLEnvironment extends JDBCEnvironment { true); } + @Override + public boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException { + + // Enforce access window restrictions for active sessions unless explicitly disabled + return getProperty( + MySQLGuacamoleProperties.MYSQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS, + true); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java index 6a5944fd3..c676feaad 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java @@ -278,4 +278,17 @@ public class MySQLGuacamoleProperties { }; + /** + * Whether or not user-specific access time windows should be enforced for active sessions, + * i.e. whether users with active sessions should be logged out immediately when an access + * window closes or the user is disabled. + */ + public static final BooleanGuacamoleProperty MYSQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "mysql-enforce-access-windows-for-active-sessions"; } + + }; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java index ac08e0ae0..5e0a5a1fe 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java @@ -371,4 +371,13 @@ public class PostgreSQLEnvironment extends JDBCEnvironment { true); } + @Override + public boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException { + + // Enforce access window restrictions for active sessions unless explicitly disabled + return getProperty( + PostgreSQLGuacamoleProperties.POSTGRESQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS, + true); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java index dfa00b68c..636c0a1c7 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java @@ -290,4 +290,17 @@ public class PostgreSQLGuacamoleProperties { }; + /** + * Whether or not user-specific access time windows should be enforced for active sessions, + * i.e. whether users with active sessions should be logged out immediately when an access + * window closes or the user is disabled. + */ + public static final BooleanGuacamoleProperty POSTGRESQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "postgresql-enforce-access-windows-for-active-sessions"; } + + }; + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java index 0b6983691..7d88d7399 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerEnvironment.java @@ -268,4 +268,13 @@ public class SQLServerEnvironment extends JDBCEnvironment { true); } + @Override + public boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException { + + // Enforce access window restrictions for active sessions unless explicitly disabled + return getProperty( + SQLServerGuacamoleProperties.SQLSERVER_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS, + true); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java index 432454ed8..e7b1c6982 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java @@ -220,4 +220,17 @@ public class SQLServerGuacamoleProperties { }; + /** + * Whether or not user-specific access time windows should be enforced for active sessions, + * i.e. whether users with active sessions should be logged out immediately when an access + * window closes or the user is disabled. + */ + public static final BooleanGuacamoleProperty SQLSERVER_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "sqlserver-enforce-access-windows-for-active-sessions"; } + + }; + } From 89945caa5f70b4a3c925fd6066c7fcbb0135fd80 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 2 Dec 2022 04:35:56 +0000 Subject: [PATCH 101/289] GUACAMOLE-1723: Simplify active window check; do not refresh from DB during active sessions. --- .../modules/guacamole-auth-jdbc-base/pom.xml | 6 - .../JDBCAuthenticationProviderModule.java | 13 +- .../guacamole/auth/jdbc/JDBCEnvironment.java | 7 +- .../ActiveConnectionDirectory.java | 6 - .../auth/jdbc/base/RestrictedObject.java | 47 ----- .../jdbc/connection/ConnectionDirectory.java | 6 - .../ConnectionGroupDirectory.java | 6 - .../SharingProfileDirectory.java | 6 - .../AbstractGuacamoleTunnelService.java | 71 ++----- .../AccessEnforcingDelegatingTunnel.java | 196 ------------------ ...ccessEnforcingDelegatingTunnelFactory.java | 54 ----- .../jdbc/tunnel/ActiveConnectionRecord.java | 7 - .../auth/jdbc/user/ModeledUserContext.java | 41 ++++ .../auth/jdbc/user/UserDirectory.java | 6 - .../jdbc/usergroup/UserGroupDirectory.java | 6 - .../mysql/conf/MySQLGuacamoleProperties.java | 2 +- .../conf/PostgreSQLGuacamoleProperties.java | 2 +- .../conf/SQLServerGuacamoleProperties.java | 2 +- .../net/auth/DelegatingUserContext.java | 5 + .../guacamole/net/auth/UserContext.java | 21 ++ .../apache/guacamole/GuacamoleSession.java | 16 ++ .../rest/auth/HashTokenSessionMap.java | 29 ++- 22 files changed, 123 insertions(+), 432 deletions(-) delete mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java delete mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml index ac9856240..9c99593bf 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml @@ -94,12 +94,6 @@ guice - - - com.google.inject.extensions - guice-assistedinject - - com.google.guava diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java index e47ccf259..5ae0ea53f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java @@ -20,8 +20,6 @@ package org.apache.guacamole.auth.jdbc; import com.google.inject.Scopes; -import com.google.inject.assistedinject.FactoryModuleBuilder; - import javax.sql.DataSource; import org.apache.guacamole.auth.jdbc.user.ModeledUserContext; import org.apache.guacamole.auth.jdbc.connectiongroup.RootConnectionGroup; @@ -40,8 +38,6 @@ import org.apache.guacamole.auth.jdbc.permission.SystemPermissionMapper; import org.apache.guacamole.auth.jdbc.user.UserMapper; import org.apache.guacamole.auth.jdbc.connectiongroup.ConnectionGroupService; import org.apache.guacamole.auth.jdbc.connection.ConnectionService; -import org.apache.guacamole.auth.jdbc.tunnel.AccessEnforcingDelegatingTunnel; -import org.apache.guacamole.auth.jdbc.tunnel.AccessEnforcingDelegatingTunnelFactory; import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService; import org.apache.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.apache.guacamole.auth.jdbc.security.SHA256PasswordEncryptionService; @@ -200,14 +196,7 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule { bind(UserGroupPermissionService.class); bind(UserPermissionService.class); bind(UserService.class); - - // Bind Factories - install(new FactoryModuleBuilder() - .implement( - AccessEnforcingDelegatingTunnel.class, - AccessEnforcingDelegatingTunnel.class) - .build(AccessEnforcingDelegatingTunnelFactory.class)); - + } } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java index 778d5b29f..77f038734 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCEnvironment.java @@ -230,12 +230,11 @@ public abstract class JDBCEnvironment extends DelegatingEnvironment { /** * Returns a boolean value representing whether access time windows should * be enforced for active connections - i.e. whether a currently-connected - * user should be disconnected upon the closure of an access window, or - * when the user is disabled. + * user should be disconnected upon the closure of an access window. * * @return - * True if a connected user should be disconnected upon an access time - * window closing, or the user being disabled. + * true if a connected user should be disconnected upon an access time + * window closing, false otherwise. * * @throws GuacamoleException * If guacamole.properties cannot be parsed. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java index b9a149933..b0d6324aa 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java @@ -44,40 +44,34 @@ public class ActiveConnectionDirectory extends RestrictedObject @Override public ActiveConnection get(String identifier) throws GuacamoleException { - validateUserAccess(); return activeConnectionService.retrieveObject(getCurrentUser(), identifier); } @Override public Collection getAll(Collection identifiers) throws GuacamoleException { - validateUserAccess(); Collection objects = activeConnectionService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @Override public Set getIdentifiers() throws GuacamoleException { - validateUserAccess(); return activeConnectionService.getIdentifiers(getCurrentUser()); } @Override public void add(ActiveConnection object) throws GuacamoleException { - validateUserAccess(); activeConnectionService.createObject(getCurrentUser(), object); } @Override public void update(ActiveConnection object) throws GuacamoleException { - validateUserAccess(); TrackedActiveConnection connection = (TrackedActiveConnection) object; activeConnectionService.updateObject(getCurrentUser(), connection); } @Override public void remove(String identifier) throws GuacamoleException { - validateUserAccess(); activeConnectionService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java index 461ce5804..66979d487 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/RestrictedObject.java @@ -19,13 +19,7 @@ package org.apache.guacamole.auth.jdbc.base; -import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleUnauthorizedException; -import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser; -import org.apache.guacamole.auth.jdbc.user.ModeledUser; - -import com.google.inject.Inject; /** * Common base class for objects that are associated with the users that @@ -39,12 +33,6 @@ public abstract class RestrictedObject { */ private ModeledAuthenticatedUser currentUser; - /** - * The environment of the Guacamole server. - */ - @Inject - private JDBCEnvironment environment; - /** * Initializes this object, associating it with the current authenticated * user and populating it with data from the given model object @@ -80,39 +68,4 @@ public abstract class RestrictedObject { this.currentUser = currentUser; } - /** - * Validate that the current user is within a valid access time window - * and not disabled. If the user account is disabled or not within a - * valid access window, a GuacamoleUnauthorizedException will be thrown. - * - * This method can be called by RestrictedObject implementations before - * any operation that's specific to the current logged in user, to make - * sure that their access is still valid and enabled. - * - * If accessWindowCheckEnabled is set to false, the check will be skipped, - * and GuacamoleUnauthorizedException will never be thrown. - * - * @throws GuacamoleException - * If the user is outside of a valid access window, the user is - * disabled, or an error occurs while trying to determine access time - * restriction configuration. - */ - protected void validateUserAccess() throws GuacamoleException { - - // If access windows shouldn't be checked for active sessions, skip - // this check entirely - if (!environment.enforceAccessWindowsForActiveSessions()) - return; - - // If the user is outside of a valid access time window or disabled, - // throw an exception to immediately log them out - ModeledUser modeledUser = getCurrentUser().getUser(); - if ( - !modeledUser.isAccountAccessible() - || !modeledUser.isAccountValid() - || modeledUser.isDisabled() - ) - throw new GuacamoleUnauthorizedException("Permission Denied."); - } - } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 54ca79033..52a127df4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -45,14 +45,12 @@ public class ConnectionDirectory extends RestrictedObject @Override public Connection get(String identifier) throws GuacamoleException { - validateUserAccess(); return connectionService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { - validateUserAccess(); Collection objects = connectionService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -60,21 +58,18 @@ public class ConnectionDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { - validateUserAccess(); return connectionService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(Connection object) throws GuacamoleException { - validateUserAccess(); connectionService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(Connection object) throws GuacamoleException { - validateUserAccess(); ModeledConnection connection = (ModeledConnection) object; connectionService.updateObject(getCurrentUser(), connection); } @@ -82,7 +77,6 @@ public class ConnectionDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { - validateUserAccess(); connectionService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java index 030432793..9f3930597 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java @@ -45,14 +45,12 @@ public class ConnectionGroupDirectory extends RestrictedObject @Override public ConnectionGroup get(String identifier) throws GuacamoleException { - validateUserAccess(); return connectionGroupService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { - validateUserAccess(); Collection objects = connectionGroupService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -60,21 +58,18 @@ public class ConnectionGroupDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { - validateUserAccess(); return connectionGroupService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(ConnectionGroup object) throws GuacamoleException { - validateUserAccess(); connectionGroupService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(ConnectionGroup object) throws GuacamoleException { - validateUserAccess(); ModeledConnectionGroup connectionGroup = (ModeledConnectionGroup) object; connectionGroupService.updateObject(getCurrentUser(), connectionGroup); } @@ -82,7 +77,6 @@ public class ConnectionGroupDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { - validateUserAccess(); connectionGroupService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java index 57ccb6d63..632557052 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java @@ -44,14 +44,12 @@ public class SharingProfileDirectory extends RestrictedObject @Override public SharingProfile get(String identifier) throws GuacamoleException { - validateUserAccess(); return sharingProfileService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { - validateUserAccess(); return Collections.unmodifiableCollection( sharingProfileService.retrieveObjects(getCurrentUser(), identifiers) ); @@ -60,21 +58,18 @@ public class SharingProfileDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { - validateUserAccess(); return sharingProfileService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(SharingProfile object) throws GuacamoleException { - validateUserAccess(); sharingProfileService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(SharingProfile object) throws GuacamoleException { - validateUserAccess(); ModeledSharingProfile sharingProfile = (ModeledSharingProfile) object; sharingProfileService.updateObject(getCurrentUser(), sharingProfile); } @@ -82,7 +77,6 @@ public class SharingProfileDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { - validateUserAccess(); sharingProfileService.deleteObject(getCurrentUser(), identifier); } 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 f26b8ea13..8c16363a6 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 @@ -46,7 +46,6 @@ import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleUpstreamException; -import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.connection.ConnectionMapper; import org.apache.guacamole.net.GuacamoleSocket; import org.apache.guacamole.net.GuacamoleTunnel; @@ -64,7 +63,6 @@ import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile; import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterMapper; import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileParameterModel; import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser; -import org.apache.guacamole.auth.jdbc.user.UserService; import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration; import org.apache.guacamole.protocol.FailoverGuacamoleSocket; import org.slf4j.Logger; @@ -119,24 +117,6 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS @Inject private SharedConnectionMap connectionMap; - /** - * Service for fetching users. - */ - @Inject - private UserService userService; - - /** - * The environment of the Guacamole server. - */ - @Inject - private JDBCEnvironment environment; - - /** - * Provider for creating AccessEnforcingDelegatingTunnel instances. - */ - @Inject - private AccessEnforcingDelegatingTunnelFactory accessEnforcingTunnelFactory; - /** * All active connections through the tunnel having a given UUID. */ @@ -405,21 +385,13 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS * connection, which MUST already be acquired via acquire(). The given * client information will be passed to guacd when the connection is * established. - * + * * The connection will be automatically released when it closes, or if it * fails to establish entirely. * - * If user access window restrictions are enabled for active sessions, - * they will be automatically enforced for any valid database users as the - * tunnel is used. - * * @param activeConnection * The active connection record of the connection in use. * - * @param user - * The user record in the database for the user connecting to the - * connection, if any. - * * @param info * Information describing the Guacamole client connecting to the given * connection. @@ -440,10 +412,8 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS * If an error occurs while the connection is being established, or * while connection configuration information is being retrieved. */ - private GuacamoleTunnel assignGuacamoleTunnel( - ActiveConnectionRecord activeConnection, - ModeledAuthenticatedUser user, GuacamoleClientInformation info, - Map tokens, + private GuacamoleTunnel assignGuacamoleTunnel(ActiveConnectionRecord activeConnection, + GuacamoleClientInformation info, Map tokens, boolean interceptErrors) throws GuacamoleException { // Record new active connection @@ -506,21 +476,12 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS getUnconfiguredGuacamoleSocket(connection.getGuacamoleProxyConfiguration(), cleanupTask), config, info); - // Assign new tunnel - GuacamoleTunnel tunnel; + // Assign and return new tunnel if (interceptErrors) - tunnel = activeConnection.assignGuacamoleTunnel(new FailoverGuacamoleSocket(socket), socket.getConnectionID()); + return activeConnection.assignGuacamoleTunnel(new FailoverGuacamoleSocket(socket), socket.getConnectionID()); else - tunnel = activeConnection.assignGuacamoleTunnel(socket, socket.getConnectionID()); - - // If there is an associated database user, and access window - // restrictions are enabled for active sessions, wrap the tunnel - // in order to check user login validity on every instruction read - if (user != null && environment.enforceAccessWindowsForActiveSessions()) { - tunnel = accessEnforcingTunnelFactory.create(tunnel, user); - } - - return tunnel; + return activeConnection.assignGuacamoleTunnel(socket, socket.getConnectionID()); + } // Execute cleanup if socket could not be created @@ -670,10 +631,8 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS acquire(user, Collections.singletonList(connection), true); // Connect only if the connection was successfully acquired - ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord( - connectionMap, user, connection); - return assignGuacamoleTunnel( - connectionRecord, user, info, tokens, false); + ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord(connectionMap, user, connection); + return assignGuacamoleTunnel(connectionRecord, info, tokens, false); } @@ -718,10 +677,9 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS try { // Connect to acquired child - ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord( - connectionMap, user, connectionGroup, connection); - GuacamoleTunnel tunnel = assignGuacamoleTunnel( - connectionRecord, user, info, tokens, connections.size() > 1); + ActiveConnectionRecord connectionRecord = new ActiveConnectionRecord(connectionMap, user, connectionGroup, connection); + GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, + info, tokens, connections.size() > 1); // If session affinity is enabled, prefer this connection going forward if (connectionGroup.isSessionAffinityEnabled()) @@ -778,10 +736,7 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS user, definition.getActiveConnection(), definition.getSharingProfile()); // Connect to shared connection described by the created record - GuacamoleTunnel tunnel = assignGuacamoleTunnel( - connectionRecord, userService.retrieveAuthenticatedUser( - user.getAuthenticationProvider(), user.getCredentials()), - info, tokens, false); + GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, info, tokens, false); // Register tunnel, such that it is closed when the // SharedConnectionDefinition is invalidated diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java deleted file mode 100644 index 5ee08330c..000000000 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnel.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.guacamole.auth.jdbc.tunnel; - -import java.util.concurrent.atomic.AtomicReference; - -import javax.annotation.Nonnull; - -import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleUnauthorizedException; -import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser; -import org.apache.guacamole.auth.jdbc.user.ModeledUser; -import org.apache.guacamole.auth.jdbc.user.UserService; -import org.apache.guacamole.io.GuacamoleReader; -import org.apache.guacamole.net.DelegatingGuacamoleTunnel; -import org.apache.guacamole.net.GuacamoleTunnel; -import org.apache.guacamole.protocol.FilteredGuacamoleReader; -import org.apache.guacamole.protocol.GuacamoleFilter; -import org.apache.guacamole.protocol.GuacamoleInstruction; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; - -/** - * A tunnel implementation that enforces access window restriction for the - * provided ModeledUser, throwing a GuacamoleUnauthorizedException if the - * user's configured access window has closed, or if the user has become - * disabled. All other tunnel implementation is delegated to the underlying - * tunnel object. - */ -public class AccessEnforcingDelegatingTunnel extends DelegatingGuacamoleTunnel { - - /** - * Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger( - AccessEnforcingDelegatingTunnel.class); - - /** - * The number of milliseconds between subsequent refreshes of the user - * from the DB. - */ - private static final long USER_MODEL_REFRESH_INTERVAL = 10000; - - /** - * The user who's access window restrictions should be applied for the - * wrapped tunnel. - */ - private final AtomicReference user; - - /** - * A thread that will continously refresh the user - */ - private final Thread userRefreshThread; - - /** - * A service to use for refreshing the user from the DB. - */ - @Inject - private UserService userService; - - /** - * Create a new tunnel that will enforce the access window restrictions of - * the provided user, during usage of the provided tunnel. - * - * @param tunnel - * The tunnel to delegate to. - * - * @param modeledAuthenticatedUser - * The user whose access restrictions should be applied. - * - */ - @AssistedInject - public AccessEnforcingDelegatingTunnel( - @Nonnull @Assisted GuacamoleTunnel tunnel, - @Nonnull @Assisted ModeledAuthenticatedUser modeledAuthenticatedUser) { - - super(tunnel); - this.user = new AtomicReference<>(modeledAuthenticatedUser.getUser()); - - this.userRefreshThread = new Thread(() -> { - while (true) { - - try { - - // Fetch an up-to-date user record from the DB to ensure - // that any access restrictions modified while this tunnel - // is open will be taken into account - this.user.set(userService.retrieveUser( - modeledAuthenticatedUser.getAuthenticationProvider(), - modeledAuthenticatedUser)); - } - - // If an error occurs while trying to fetch the updated user, - // log the warning / exception and stop the refresh thread - catch (GuacamoleException e) { - - logger.warn( - "Aborting user refresh thread due to error: {}", - e.getMessage()); - logger.debug( - "Exception caught while attempting to refresh user.", e); - - return; - } - - try { - - // Wait a bit before refreshing the user record again - Thread.sleep(USER_MODEL_REFRESH_INTERVAL); - } - - // If interrupted by the tunnel, exit immediately - catch (InterruptedException e) { - return; - } - } - }); - } - - @Override - public GuacamoleReader acquireReader() { - - // Start periodically refreshing the user record - userRefreshThread.start(); - - // Filter received instructions, checking if the user's login - // is still valid for each one. If the login is invalid, - // log them out immediately and close the tunnel. - return new FilteredGuacamoleReader( - super.acquireReader(), - new GuacamoleFilter() { - - @Override - public GuacamoleInstruction filter( - GuacamoleInstruction instruction) throws GuacamoleException { - - // The user record, at most USER_MODEL_REFRESH_INTERVAL - // milliseconds old - ModeledUser modeledUser = user.get(); - - // If the user is outside of a valid access time window, - // or disabled, throw an exception to immediately log them out - if ( - !modeledUser.isAccountAccessible() - || !modeledUser.isAccountValid() - || modeledUser.isDisabled() - ) { - throw new GuacamoleUnauthorizedException("Permission Denied."); - } - - return instruction; - } - } - ); - } - - @Override - public void releaseReader() { - - // Interrupt the refresh thread; it will clean itself up - userRefreshThread.interrupt(); - - super.releaseReader(); - } - - @Override - public void close() throws GuacamoleException { - - // Interrupt the refresh thread; it will clean itself up - userRefreshThread.interrupt(); - - super.close(); - } - -} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java deleted file mode 100644 index 0c6350766..000000000 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AccessEnforcingDelegatingTunnelFactory.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.guacamole.auth.jdbc.tunnel; - -import javax.annotation.Nonnull; - -import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser; -import org.apache.guacamole.net.GuacamoleTunnel; - -/** - * A factory for creating AccessEnforcingDelegatingTunnel instances. - */ -public interface AccessEnforcingDelegatingTunnelFactory { - - /** - * Create and return a new AccessEnforcingDelegatingTunnel wrapping the - * provided tunnel and enforcing the user restrictions associated with the - * provided user. - * - * @param tunnel - * The tunnel to delegate to. - * - * @param user - * The user whose access window restrictions should be applied for the - * wrapped tunel. - * - * @return - * A new AccessEnforcingDelegatingTunnel wrapping the provided tunnel - * and enforcing the user restrictions associated with the provided - * user. - * - */ - public AccessEnforcingDelegatingTunnel create( - @Nonnull GuacamoleTunnel tunnel, - @Nonnull ModeledAuthenticatedUser user); - -} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java index df25159a9..c3150ca97 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java @@ -21,9 +21,6 @@ package org.apache.guacamole.auth.jdbc.tunnel; import java.util.Date; import java.util.UUID; - -import javax.annotation.Nonnull; - import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordModel; import org.apache.guacamole.auth.jdbc.connection.ModeledConnection; import org.apache.guacamole.auth.jdbc.connection.ModeledConnectionRecord; @@ -380,8 +377,6 @@ public class ActiveConnectionRecord extends ModeledConnectionRecord { * @return * The newly-created tunnel associated with this connection record. */ - @Nonnull - @SuppressWarnings("null") public GuacamoleTunnel assignGuacamoleTunnel(final GuacamoleSocket socket, String connectionID) { @@ -405,8 +400,6 @@ public class ActiveConnectionRecord extends ModeledConnectionRecord { this.connectionID = connectionID; // Return newly-created tunnel - // NOTE: Guaranteed to be non-null since it was just set in this - // method, and there is no functionality to set it to null return this.tunnel; } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java index 1416b0b39..e5ad5d2ea 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledUserContext.java @@ -29,6 +29,7 @@ import java.util.Collection; import java.util.Date; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.jdbc.base.RestrictedObject; +import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.activeconnection.ActiveConnectionDirectory; import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel; import org.apache.guacamole.auth.jdbc.connection.ConnectionRecordSet; @@ -50,6 +51,8 @@ import org.apache.guacamole.net.auth.SharingProfile; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * UserContext implementation which is driven by an arbitrary, underlying @@ -58,6 +61,11 @@ import org.apache.guacamole.net.auth.UserGroup; public class ModeledUserContext extends RestrictedObject implements org.apache.guacamole.net.auth.UserContext { + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(ModeledUserContext.class); + /** * User directory restricted by the permissions of the user associated * with this context. @@ -130,6 +138,12 @@ public class ModeledUserContext extends RestrictedObject @Inject private UserRecordMapper userRecordMapper; + /** + * The environment of the Guacamole server. + */ + @Inject + private JDBCEnvironment environment; + /** * The activity record associated with this user's Guacamole session. If * this user's session will not have an associated activity record, such as @@ -296,4 +310,31 @@ public class ModeledUserContext extends RestrictedObject } + @Override + public boolean isValid() { + + try { + // If access window enforcement is disabled for active sessions, + // skip validity checks entirely + if (!environment.enforceAccessWindowsForActiveSessions()) + return true; + } + + catch (GuacamoleException e) { + + logger.warn( + "Unable to determine if access window enforcement is" + + " enabled for active sessions; enforcing by default: {}" + , e.getMessage()); + logger.debug("Unable to determine access window enforcement policy.", e); + + } + + // A user context is valid if the associated user's account is valid + // for the current date, and the user is within an access time window + ModeledUser user = getCurrentUser().getUser(); + return user.isAccountValid() && user.isAccountAccessible(); + + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java index db8bea12f..dffd8e2ec 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java @@ -45,14 +45,12 @@ public class UserDirectory extends RestrictedObject @Override public User get(String identifier) throws GuacamoleException { - validateUserAccess(); return userService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { - validateUserAccess(); Collection objects = userService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -60,21 +58,18 @@ public class UserDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { - validateUserAccess(); return userService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(User object) throws GuacamoleException { - validateUserAccess(); userService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(User object) throws GuacamoleException { - validateUserAccess(); ModeledUser user = (ModeledUser) object; userService.updateObject(getCurrentUser(), user); } @@ -82,7 +77,6 @@ public class UserDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { - validateUserAccess(); userService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java index f61af0bea..911b8521f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java @@ -44,14 +44,12 @@ public class UserGroupDirectory extends RestrictedObject @Override public UserGroup get(String identifier) throws GuacamoleException { - validateUserAccess(); return userGroupService.retrieveObject(getCurrentUser(), identifier); } @Override @Transactional public Collection getAll(Collection identifiers) throws GuacamoleException { - validateUserAccess(); Collection objects = userGroupService.retrieveObjects(getCurrentUser(), identifiers); return Collections.unmodifiableCollection(objects); } @@ -59,21 +57,18 @@ public class UserGroupDirectory extends RestrictedObject @Override @Transactional public Set getIdentifiers() throws GuacamoleException { - validateUserAccess(); return userGroupService.getIdentifiers(getCurrentUser()); } @Override @Transactional public void add(UserGroup object) throws GuacamoleException { - validateUserAccess(); userGroupService.createObject(getCurrentUser(), object); } @Override @Transactional public void update(UserGroup object) throws GuacamoleException { - validateUserAccess(); ModeledUserGroup group = (ModeledUserGroup) object; userGroupService.updateObject(getCurrentUser(), group); } @@ -81,7 +76,6 @@ public class UserGroupDirectory extends RestrictedObject @Override @Transactional public void remove(String identifier) throws GuacamoleException { - validateUserAccess(); userGroupService.deleteObject(getCurrentUser(), identifier); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java index c676feaad..ca17757a4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java @@ -281,7 +281,7 @@ public class MySQLGuacamoleProperties { /** * Whether or not user-specific access time windows should be enforced for active sessions, * i.e. whether users with active sessions should be logged out immediately when an access - * window closes or the user is disabled. + * window closes. */ public static final BooleanGuacamoleProperty MYSQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS = new BooleanGuacamoleProperty() { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java index 636c0a1c7..3f33ec44f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java @@ -293,7 +293,7 @@ public class PostgreSQLGuacamoleProperties { /** * Whether or not user-specific access time windows should be enforced for active sessions, * i.e. whether users with active sessions should be logged out immediately when an access - * window closes or the user is disabled. + * window closes. */ public static final BooleanGuacamoleProperty POSTGRESQL_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS = new BooleanGuacamoleProperty() { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java index e7b1c6982..1f2500350 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/java/org/apache/guacamole/auth/sqlserver/conf/SQLServerGuacamoleProperties.java @@ -223,7 +223,7 @@ public class SQLServerGuacamoleProperties { /** * Whether or not user-specific access time windows should be enforced for active sessions, * i.e. whether users with active sessions should be logged out immediately when an access - * window closes or the user is disabled. + * window closes. */ public static final BooleanGuacamoleProperty SQLSERVER_ENFORCE_ACCESS_WINDOWS_FOR_ACTIVE_SESSIONS = new BooleanGuacamoleProperty() { diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java index 4b0343181..7431fa899 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingUserContext.java @@ -162,4 +162,9 @@ public class DelegatingUserContext implements UserContext { return userContext.getPrivileged(); } + @Override + public boolean isValid() { + return userContext.isValid(); + } + } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java index 3f75b899c..a9ea7ea0c 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/UserContext.java @@ -40,6 +40,27 @@ public interface UserContext { */ User self(); + /** + * Returns true if the session for the User associated with this user + * context is valid, or false otherwise. If the session is not valid, + * the webapp can be expected to terminate the session within a short + * period of time. + * + * NOTE: The webapp currently checks once a minute, and terminates any + * session marked as invalid. + * + * @return + * true if the session for the User associated with this user + * context is valid, or false otherwise. + */ + default boolean isValid() { + + // A user context is always valid unless explicitly updated by an + // implementation + return true; + + } + /** * Returns an arbitrary REST resource representing this UserContext. The * REST resource returned must be properly annotated with JSR-311 diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java index 3e8d488c3..0458639d1 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleSession.java @@ -132,6 +132,22 @@ public class GuacamoleSession { return Collections.unmodifiableList(userContexts); } + /** + * Returns true if all user contexts associated with this session are + * valid, or false if any user context is not valid. If a session is not + * valid, it may no longer be used, and invalidate() should be invoked. + * + * @return + * true if all user contexts associated with this session are + * valid, or false if any user context is not valid. + */ + public boolean isValid() { + + // Immediately return false if any user context is not valid + return !userContexts.stream().anyMatch( + userContext -> !userContext.isValid()); + } + /** * Returns the UserContext associated with this session that originated * from the AuthenticationProvider with the given identifier. If no such diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java index 24f653098..15d11eb62 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/HashTokenSessionMap.java @@ -94,7 +94,8 @@ public class HashTokenSessionMap implements TokenSessionMap { /** * Task which iterates through all active sessions, evicting those sessions - * which are beyond the session timeout. + * which are beyond the session timeout, or are marked as invalid by an + * extension. */ private class SessionEvictionTask implements Runnable { @@ -105,7 +106,8 @@ public class HashTokenSessionMap implements TokenSessionMap { /** * Creates a new task which automatically evicts sessions which are - * older than the specified timeout. + * older than the specified timeout, or are marked as invalid by an + * extension. * * @param sessionTimeout The maximum age of any session, in * milliseconds. @@ -116,16 +118,16 @@ public class HashTokenSessionMap implements TokenSessionMap { /** * Iterates through all active sessions, evicting those sessions which - * are beyond the session timeout. Internal errors which would - * otherwise stop the session eviction process are caught, logged, and - * the process is allowed to proceed. + * are beyond the session timeout, or are marked as invalid. Internal + * errors which would otherwise stop the session eviction process are + * caught, logged, and the process is allowed to proceed. */ - private void evictExpiredSessions() { + private void evictExpiredOrInvalidSessions() { // Get start time of session check time long sessionCheckStart = System.currentTimeMillis(); - logger.debug("Checking for expired sessions..."); + logger.debug("Checking for expired or invalid sessions..."); // For each session, remove sesions which have expired Iterator> entries = sessionMap.entrySet().iterator(); @@ -136,6 +138,15 @@ public class HashTokenSessionMap implements TokenSessionMap { try { + // Invalidate any sessions which have been flagged as invalid by extensions + if (!session.isValid()) { + logger.debug( + "Session \"{}\" has been invalidated by an extension.", + entry.getKey()); + entries.remove(); + session.invalidate(); + } + // Do not expire sessions which are active if (session.hasTunnels()) continue; @@ -170,13 +181,13 @@ public class HashTokenSessionMap implements TokenSessionMap { @Override public void run() { - // The evictExpiredSessions() function should already + // The evictExpiredOrInvalidSessions() function should already // automatically handle and log all unexpected internal errors, // but wrap the entire call in a try/catch plus additional logging // to ensure that absolutely no errors can result in the entire // thread dying try { - evictExpiredSessions(); + evictExpiredOrInvalidSessions(); } catch (Throwable t) { logger.error("An unexpected internal error prevented the " From bb5aea96f133c6dbe8589b92c1d383b9bef676fa Mon Sep 17 00:00:00 2001 From: Alex Leitner Date: Wed, 28 Dec 2022 20:15:26 +0000 Subject: [PATCH 102/289] GUACAMOLE-1728: Allow Null User Object attributes to be saved. --- .../guacamole/rest/directory/DirectoryObjectTranslator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectTranslator.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectTranslator.java index f3fe75564..b8af563a5 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectTranslator.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryObjectTranslator.java @@ -145,8 +145,9 @@ public abstract class DirectoryObjectTranslator Date: Wed, 4 Jan 2023 15:32:34 +0100 Subject: [PATCH 103/289] GUACAMOLE-1730: Add polish translation. --- .../src/main/resources/guac-manifest.json | 3 +- .../src/main/resources/translations/pl.json | 5 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/translations/pl.json | 13 + .../src/main/resources/translations/pl.json | 113 ++ .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/translations/pl.json | 18 + .../src/main/resources/translations/pl.json | 26 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/translations/pl.json | 43 + .../src/main/resources/guac-manifest.json | 1 + .../src/main/resources/translations/pl.json | 14 + .../main/frontend/src/translations/pl.json | 1051 +++++++++++++++++ 19 files changed, 1295 insertions(+), 1 deletion(-) create mode 100644 extensions/guacamole-auth-ban/src/main/resources/translations/pl.json create mode 100644 extensions/guacamole-auth-duo/src/main/resources/translations/pl.json create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/pl.json create mode 100644 extensions/guacamole-auth-quickconnect/src/main/resources/translations/pl.json create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/pl.json create mode 100644 extensions/guacamole-auth-totp/src/main/resources/translations/pl.json create mode 100644 extensions/guacamole-history-recording-storage/src/main/resources/translations/pl.json create mode 100644 guacamole/src/main/frontend/src/translations/pl.json diff --git a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json index 0133a7364..2acdb0efd 100644 --- a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json @@ -10,7 +10,8 @@ ], "translations" : [ - "translations/en.json" + "translations/en.json", + "translations/pl.json" ] } diff --git a/extensions/guacamole-auth-ban/src/main/resources/translations/pl.json b/extensions/guacamole-auth-ban/src/main/resources/translations/pl.json new file mode 100644 index 000000000..0ba1f80c5 --- /dev/null +++ b/extensions/guacamole-auth-ban/src/main/resources/translations/pl.json @@ -0,0 +1,5 @@ +{ + "LOGIN": { + "ERROR_TOO_MANY_ATTEMPTS" : "Zbyt wiele nieudanych prób logowania. Spróbuj ponownie później." + } +} 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 fe1043a63..45a278248 100644 --- a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json @@ -16,6 +16,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.json", "translations/ru.json", "translations/zh.json" diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/pl.json b/extensions/guacamole-auth-duo/src/main/resources/translations/pl.json new file mode 100644 index 000000000..dd4d3b88a --- /dev/null +++ b/extensions/guacamole-auth-duo/src/main/resources/translations/pl.json @@ -0,0 +1,13 @@ +{ + + "DATA_SOURCE_DUO" : { + "NAME" : "Duo TFA Backend" + }, + + "LOGIN" : { + "FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "", + "INFO_DUO_VALIDATION_CODE_INCORRECT" : "Nieprawidłowy kod weryfikacyjny Duo.", + "INFO_DUO_AUTH_REQUIRED" : "Aby kontynuować uwierzytelnij się w Duo." + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/pl.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/pl.json new file mode 100644 index 000000000..8c31932aa --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/pl.json @@ -0,0 +1,113 @@ +{ + + "LOGIN" : { + + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", + "ERROR_PASSWORD_SAME" : "Nowe hasło musi być inne od tego, które wygasło.", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_NOT_VALID" : "To konto użytkownika nie jest poprawne.", + "ERROR_NOT_ACCESSIBLE" : "Dostęp do tego konta jest obecnie zablokowany. Spróbuj ponownie później.", + + "INFO_PASSWORD_EXPIRED" : "Twoje hasło wygasło i należy je zresetować. Wprowadź nowe hasło, aby kontynuować.", + + "FIELD_HEADER_NEW_PASSWORD" : "Nowe hasło", + "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "Potwierdź nowe hasło" + + }, + + "CONNECTION_ATTRIBUTES" : { + + "FIELD_HEADER_MAX_CONNECTIONS" : "Maksymalna liczba połączeń:", + "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Maksymalna liczba połączeń dla jednego użytkownika:", + + "FIELD_HEADER_FAILOVER_ONLY" : "Używaj tylko do przełączania awaryjnego:", + "FIELD_HEADER_WEIGHT" : "Waga połączenia:", + + "FIELD_HEADER_GUACD_HOSTNAME" : "Nazwa Hosta:", + "FIELD_HEADER_GUACD_ENCRYPTION" : "Szyfrowanie:", + "FIELD_HEADER_GUACD_PORT" : "Port:", + + "FIELD_OPTION_GUACD_ENCRYPTION_EMPTY" : "", + "FIELD_OPTION_GUACD_ENCRYPTION_NONE" : "Brak (nieszyfrowane)", + "FIELD_OPTION_GUACD_ENCRYPTION_SSL" : "SSL / TLS", + + "SECTION_HEADER_CONCURRENCY" : "Limity Jednoczesnych Połączeń", + "SECTION_HEADER_LOAD_BALANCING" : "Równoważenie Obciążenia", + "SECTION_HEADER_GUACD" : "Parametry Guacamole Proxy (guacd)" + + }, + + "CONNECTION_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_ENABLE_SESSION_AFFINITY" : "Włącz powiązanie sesji:", + "FIELD_HEADER_MAX_CONNECTIONS" : "Maksymalna liczba połączeń:", + "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "Maksymalna liczba połączeń dla jednego użytkownika:", + + "SECTION_HEADER_CONCURRENCY" : "Limity Jednoczesnych Połączeń (Grupy Równoważenia Obciążenia)" + + }, + + "DATA_SOURCE_MYSQL" : { + "NAME" : "MySQL" + }, + + "DATA_SOURCE_MYSQL_SHARED" : { + "NAME" : "Współdzielone połączenia (MySQL)" + }, + + "DATA_SOURCE_POSTGRESQL" : { + "NAME" : "PostgreSQL" + }, + + "DATA_SOURCE_POSTGRESQL_SHARED" : { + "NAME" : "Współdzielone połączenia (PostgreSQL)" + }, + + "DATA_SOURCE_SQLSERVER" : { + "NAME" : "SQL Server" + }, + + "DATA_SOURCE_SQLSERVER_SHARED" : { + "NAME" : "Współdzielone połączenia (SQL Server)" + }, + + "HOME" : { + "INFO_SHARED_BY" : "Współdzielone przez {USERNAME}" + }, + + "PASSWORD_POLICY" : { + + "ERROR_CONTAINS_USERNAME" : "Hasła nie mogą zawierać nazwy użytkownika.", + "ERROR_REQUIRES_DIGIT" : "Hasła muszą zawierać przynajmniej jedną cyfrę.", + "ERROR_REQUIRES_MULTIPLE_CASE" : "Hasła muszą zawierać zarówno wielkie, jak i małe litery.", + "ERROR_REQUIRES_NON_ALNUM" : "Hasła muszą zawierać co najmniej jeden znak specjalny.", + "ERROR_REUSED" : "To hasło zostało już użyte. Nie można ponownie użyć żadnego z poprzednich {HISTORY_SIZE} {HISTORY_SIZE, plural, one{hasła} other{haseł}}.", + "ERROR_TOO_SHORT" : "Hasło musi mieć co najmniej {LENGTH} {LENGTH, plural, one{znak} other{znaków}} długości.", + "ERROR_TOO_YOUNG" : "Hasło do tego konta zostało już zresetowane. Poczekać przynajmniej {WAIT} {WAIT, plural, one{dzień} other{dni}} dłużej przed ponowną zmianą hasła." + + }, + + "USER_ATTRIBUTES" : { + + "FIELD_HEADER_DISABLED" : "Logowanie zablokowane:", + "FIELD_HEADER_EXPIRED" : "Hasło wygasło:", + "FIELD_HEADER_ACCESS_WINDOW_END" : "Nie zezwalaj na dostęp po:", + "FIELD_HEADER_ACCESS_WINDOW_START" : "Zezwalaj na dostęp od:", + "FIELD_HEADER_TIMEZONE" : "Strefa czasowa użytkownika:", + "FIELD_HEADER_VALID_FROM" : "Włącz konto od:", + "FIELD_HEADER_VALID_UNTIL" : "Wyłącz konto po:", + + "SECTION_HEADER_RESTRICTIONS" : "Ograniczenia Konta", + "SECTION_HEADER_PROFILE" : "Profil" + + }, + + "USER_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_DISABLED" : "Wyłączona:", + + "SECTION_HEADER_RESTRICTIONS" : "Ograniczenia Grupy" + + } + +} 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 28f35a96b..eab708f56 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,6 +26,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.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 00e052a8e..49d4be358 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,6 +26,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.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 860b66837..0ac4016fc 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,6 +26,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.json", "translations/ru.json", "translations/zh.json" 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 b61baae1c..3c8f9b744 100644 --- a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json @@ -27,6 +27,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.json", "translations/ru.json", "translations/zh.json" diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/translations/pl.json b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/pl.json new file mode 100644 index 000000000..e213f94b6 --- /dev/null +++ b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/pl.json @@ -0,0 +1,18 @@ +{ + + "DATA_SOURCE_QUICKCONNECT" : { + "NAME" : "QuickConnect" + }, + + "QUICKCONNECT" : { + "ACTION_CONNECT" : "Połącz", + + "ERROR_INVALID_URI" : "Podano nieprawidłowy URI", + "ERROR_NO_HOST" : "Nie podano hosta", + "ERROR_NO_PROTOCOL" : "Nie podano protokołu", + "ERROR_NOT_ABSOLUTE_URI" : "Adres URI nie jest absolutny", + + "FIELD_PLACEHOLDER_URI" : "Podaj URI Połączenia" + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/pl.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/pl.json new file mode 100644 index 000000000..9e391ecf9 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/pl.json @@ -0,0 +1,26 @@ +{ + + "DATA_SOURCE_CAS" : { + "NAME" : "CAS SSO Backend" + }, + + "DATA_SOURCE_OPENID" : { + "NAME" : "OpenID SSO Backend" + }, + + "DATA_SOURCE_SAML" : { + "NAME" : "SAML SSO Backend" + }, + + "LOGIN" : { + "FIELD_HEADER_ID_TOKEN" : "", + "FIELD_HEADER_STATE" : "", + "FIELD_HEADER_TICKET" : "", + "INFO_IDP_REDIRECT_PENDING" : "Proszę czekać, przekierowanie do dostawcy tożsamości...", + "NAME_IDP_CAS" : "CAS", + "NAME_IDP_OPENID" : "OpenID", + "NAME_IDP_SAML" : "SAML", + "SECTION_HEADER_SSO_OPTIONS" : "Zaloguj z:" + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/guac-manifest.json index aec7870a9..b663659aa 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/guac-manifest.json @@ -25,6 +25,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.json", "translations/ru.json", "translations/zh.json" diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/guac-manifest.json index 1b7a2a162..1cd83739f 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/guac-manifest.json @@ -25,6 +25,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.json", "translations/ru.json", "translations/zh.json" diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/guac-manifest.json index 10e1c74aa..67e56cf57 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/guac-manifest.json @@ -25,6 +25,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.json", "translations/ru.json", "translations/zh.json" 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 6bb04ff2b..5b5e5f191 100644 --- a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json @@ -16,6 +16,7 @@ "translations/fr.json", "translations/ja.json", "translations/ko.json", + "translations/pl.json", "translations/pt.json", "translations/ru.json", "translations/zh.json" diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/pl.json b/extensions/guacamole-auth-totp/src/main/resources/translations/pl.json new file mode 100644 index 000000000..208a1e8d4 --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/resources/translations/pl.json @@ -0,0 +1,43 @@ +{ + + "DATA_SOURCE_TOTP" : { + "NAME" : "TOTP TFA Backend" + }, + + "LOGIN" : { + "FIELD_HEADER_GUAC_TOTP" : "" + }, + + "TOTP" : { + + "ACTION_HIDE_DETAILS" : "Ukryj", + "ACTION_SHOW_DETAILS" : "Pokaż", + + "FIELD_HEADER_ALGORITHM" : "Algorytm:", + "FIELD_HEADER_DIGITS" : "Cyfry:", + "FIELD_HEADER_INTERVAL" : "Interwał:", + "FIELD_HEADER_SECRET_KEY" : "Sekretny klucz:", + + "FIELD_PLACEHOLDER_CODE" : "Kod weryfikacyjny", + + "INFO_CODE_REQUIRED" : "Podaj kod weryfikacyjny, aby potwierdzić swoją tożsamość.", + "INFO_ENROLL_REQUIRED" : "Uwierzytelnianie wieloskładnikowe zostało włączone na Twoim koncie.", + "INFO_VERIFICATION_FAILED" : "Weryfikacja nie powiodła się. Spróbuj ponownie.", + + "HELP_ENROLL_BARCODE" : "Aby zakończyć proces rejestracji, zeskanuj poniższy kod kreskowy za pomocą aplikacji do uwierzytelniania dwuskładnikowego na swoim telefonie lub urządzeniu.", + "HELP_ENROLL_VERIFY" : "Po zeskanowaniu kodu kreskowego, wprowadź {DIGITS}-cyfrowy kod weryfikacyjny aby potwierdzić, że proces rejestracji przebiegł pomyślnie.", + + "SECTION_HEADER_DETAILS" : "Szczegóły:" + + }, + + "USER_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Wygenerowano klucz:", + "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Potwierdzono urządzenie uwierzytelniające:", + + "SECTION_HEADER_TOTP_ENROLLMENT_STATUS" : "Status Rejestracji TOTP" + + } + +} diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json index 51422359d..943e6597c 100644 --- a/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json @@ -11,6 +11,7 @@ "translations" : [ "translations/en.json" + "translations/pl.json" ] } diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/translations/pl.json b/extensions/guacamole-history-recording-storage/src/main/resources/translations/pl.json new file mode 100644 index 000000000..fc8f63f4a --- /dev/null +++ b/extensions/guacamole-history-recording-storage/src/main/resources/translations/pl.json @@ -0,0 +1,14 @@ +{ + + "DATA_SOURCE_RECORDING_STORAGE" : { + "NAME" : "Session Recording Storage" + }, + + "RECORDING_STORAGE" : { + "INFO_GUACAMOLE_SESSION_RECORDING" : "Graficzne nagranie sesji zdalnego pulpitu", + "INFO_SERVER_LOG" : "Serwer/system log", + "INFO_TYPESCRIPT" : "Tesktowe nagranie sesji", + "INFO_TYPESCRIPT_TIMING" : "Informacje o czasie dla tesktowego nagrania sesji" + } + +} diff --git a/guacamole/src/main/frontend/src/translations/pl.json b/guacamole/src/main/frontend/src/translations/pl.json new file mode 100644 index 000000000..00afb3e70 --- /dev/null +++ b/guacamole/src/main/frontend/src/translations/pl.json @@ -0,0 +1,1051 @@ +{ + + "NAME" : "Polski", + + "APP" : { + + "NAME" : "Apache Guacamole", + "VERSION" : "${project.version}", + + "ACTION_ACKNOWLEDGE" : "OK", + "ACTION_CANCEL" : "Anuluj", + "ACTION_CLONE" : "Klonuj", + "ACTION_CONTINUE" : "Kontynuuj", + "ACTION_DELETE" : "Usuń", + "ACTION_DELETE_SESSIONS" : "Zakończ Sesję", + "ACTION_DOWNLOAD" : "Pobierz", + "ACTION_LOGIN" : "Zaloguj", + "ACTION_LOGIN_AGAIN" : "Zaloguj Ponownie", + "ACTION_LOGOUT" : "Wyloguj", + "ACTION_MANAGE_CONNECTIONS" : "Połączenia", + "ACTION_MANAGE_PREFERENCES" : "Preferencje", + "ACTION_MANAGE_SETTINGS" : "Ustawienia", + "ACTION_MANAGE_SESSIONS" : "Aktywne Sesje", + "ACTION_MANAGE_USERS" : "Użytkownicy", + "ACTION_MANAGE_USER_GROUPS" : "Grupy", + "ACTION_NAVIGATE_BACK" : "Wstecz", + "ACTION_NAVIGATE_HOME" : "Główna", + "ACTION_PAUSE" : "Pauza", + "ACTION_PLAY" : "Odtwórz", + "ACTION_SAVE" : "Zapisz", + "ACTION_SEARCH" : "Szukaj", + "ACTION_SHARE" : "Udostępnij", + "ACTION_UPDATE_PASSWORD" : "Zmień Hasło", + "ACTION_VIEW_HISTORY" : "Historia", + "ACTION_VIEW_RECORDING" : "Obejrzyj", + + "DIALOG_HEADER_ERROR" : "Błąd", + + "ERROR_PAGE_UNAVAILABLE" : "Wystąpił błąd i ta akcja nie może zostać zakończona. Jeśli problem nie zniknie, powiadom administratora systemu.", + "ERROR_PASSWORD_BLANK" : "Hasło nie może być puste.", + "ERROR_PASSWORD_MISMATCH" : "Podane hasła nie pasują do siebie.", + + "FIELD_HEADER_PASSWORD" : "Hasło:", + "FIELD_HEADER_PASSWORD_AGAIN" : "Powtórz Hasło:", + + "FIELD_PLACEHOLDER_FILTER" : "Filtruj", + + "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss", + + "INFO_ACTIVE_USER_COUNT" : "Aktualnie w użyciu przez {USERS} {USERS, plural, one{użytkownika} other{użytkowników}}.", + "INFO_LOGGED_OUT" : "Zostałeś wylogowany.", + + "TEXT_ANONYMOUS_USER" : "Anonimowy", + "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{sekunda} other{sekund(-y)}}} minute{{VALUE, plural, one{minuta} other{minut(y)}}} hour{{VALUE, plural, one{godzina} other{godzin(y)}}} day{{VALUE, plural, one{dzień} other{dni}}} other{}}", + "TEXT_UNTRANSLATED" : "{MESSAGE}" + + }, + + "CLIENT" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Wyczyść", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_DISCONNECT" : "Rozłącz", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_RECONNECT" : "Połącz Ponownie", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", + "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_UPLOAD_FILES" : "Wyślij Pliki", + + "DIALOG_HEADER_CONNECTING" : "Łączenie", + "DIALOG_HEADER_CONNECTION_ERROR" : "Błąd Połączenia", + "DIALOG_HEADER_DISCONNECTED" : "Rozłączono", + + "ERROR_CLIENT_201" : "Połączenie zostało zamknięte, ponieważ serwer jest zajęty. Odczekaj kilka minut i spróbuj ponownie.", + "ERROR_CLIENT_202" : "Serwer Guacamole zamknął połączenie, ponieważ zdalny pulpit nie odpowiada przez zbyt długi czas. Spróbuj ponownie lub skontaktuj się z administratorem.", + "ERROR_CLIENT_203" : "Serwer zdalnego pulpitu napotkał błąd i zamknął połączenie. Spróbuj ponownie lub skontaktuj się z administratorem.", + "ERROR_CLIENT_207" : "Serwer zdalnego pulpitu jest aktualnie nieosiągalny. Jeśli problem nie zniknie powiadom administratora systemu lub sprawdź log systemu.", + "ERROR_CLIENT_208" : "Serwer zdalnego pulpitu jest aktualnie nieosiągalny. Jeśli problem nie zniknie powiadom administratora systemu lub sprawdź log systemu.", + "ERROR_CLIENT_209" : "Serwer zdalnego pulpitu zamknął połączenie, ponieważ koliduje z innym połączeniem. Spróbuj ponownie później.", + "ERROR_CLIENT_20A" : "Serwer zdalnego pulpitu zamknął połączenie, ponieważ wydaje się być nieaktywne. Jeśli nie jest to pożądane zachowanie powiadom administratora systemu lub sprawdź ustawienia.", + "ERROR_CLIENT_20B" : "Serwer zdalnego pulpitu wymusił zamknięcie połączenia. Jeśli nie jest to pożądane zachowanie powiadom administratora systemu lub sprawdź log systemu.", + "ERROR_CLIENT_301" : "Logowanie nieudane. Spróbuj ponownie.", + "ERROR_CLIENT_303" : "Serwer zdalnego pulpitu nie zezwolił na połączenie. Poproś administratora systemu o nadanie uprawnień dla Twojego konta lub sprawdź ustawienia.", + "ERROR_CLIENT_308" : "Serwer Guacamole zamknął połączenie, ponieważ nie otrzymał odpowiedzi od Twojej przeglądarki przez zbyt długi czas. Zazwyczaj jest to spowodowane niską szybkością i jakością połączenia. Sprawdź swoje ustawienia sieci i spróbuj ponownie.", + "ERROR_CLIENT_31D" : "Serwer Guacamole nie zezwolił na połączenie, ponieważ wykorzystałeś limit jednoczesnych połączeń dla pojedynczego użytkownika. Zamknij jedno lub kilka połączeń i spróbuj ponownie.", + "ERROR_CLIENT_DEFAULT" : "Wystąpił błąd wewnętrzny serwera Guacamole i połączenie zostało zakończone. Jeśli problem nie zniknie, powiadom administratora systemu lub sprawdź log systemu.", + + "ERROR_TUNNEL_201" : "Serwer Guacamole odrzucił tę próbę połączenia, ponieważ istnieje zbyt wiele aktywnych połączeń. Poczekaj kilka minut i spróbuj ponownie.", + "ERROR_TUNNEL_202" : "Połączenie zostało zamknięte, ponieważ serwer nie odpowiada przez zbyt długo czas. Zazwyczaj spowodowane jest to niską szybkością i jakością połączenia. Sprawdź swoje ustawienia sieci i spróbuj ponownie lub poinformuj administratora systemu.", + "ERROR_TUNNEL_203" : "Serwer napotkał błąd i zamknął połączenie. Spróbuj ponownie lub skontaktuj się z administratorem systemu", + "ERROR_TUNNEL_204" : "Żądane połączenie nie istnieje. Sprawdź nazwę połączenia i spróbuj ponownie", + "ERROR_TUNNEL_205" : "To połączenie jest aktualnie w użyciu oraz jednoczesny dostęp dla tego połączenia nie jest dozwolony. Spróbuj ponownie później.", + "ERROR_TUNNEL_207" : "Serwer Guacamole jest aktualnie nieosiągalny. Sprawdź swoją sieć i spróbuj ponownie.", + "ERROR_TUNNEL_208" : "Serwer Guacamole nie akceptuje połączeń. Sprawdź swoją sieć i spróbuj ponownie.", + "ERROR_TUNNEL_301" : "Nie masz dostępu do tego połączenia, ponieważ nie jesteś zalogowany. Zaloguj się i spróbuj ponownie.", + "ERROR_TUNNEL_303" : "Nie masz uprawnień do tego połączenia. Jeśli potrzebujesz uprawnień poproś administratora systemu o dodanie Cię do listy użytkowników z zezwoleniem lub sprawdź ustawienia systemu. ", + "ERROR_TUNNEL_308" : "Serwer Guacamole zamknął połączenie, ponieważ nie otrzymał odpowiedzi od Twojej przeglądarki przez zbyt długi czas. Zazwyczaj jest to spowodowane niską szybkością i jakością połączenia. Sprawdź swoje ustawienia sieci i spróbuj ponownie.", + "ERROR_TUNNEL_31D" : "Serwer Guacamole nie zezwolił na połączenie, ponieważ wykorzystałeś limit jednoczesnych połączeń dla pojedynczego użytkownika. Zamknij jedno lub kilka połączeń i spróbuj ponownie.", + "ERROR_TUNNEL_DEFAULT" : "Wystąpił błąd wewnętrzny serwera Guacamole i połączenie zostało zakończone. Jeśli problem nie zniknie, powiadom administratora systemu lub sprawdź log systemu.", + + "ERROR_UPLOAD_100" : "Transfer plików jest nie wspierany lub wyłączony. Skontaktuj się z administratorem systemu lub sprawdź log systemu.", + "ERROR_UPLOAD_201" : "Zbyt wiele plików jest aktualnie transferowanych. Poczekaj na zakończenie trwających transferów i spróbuj ponownie.", + "ERROR_UPLOAD_202" : "Plik nie może zostać przesłany, ponieważ Serwer zdalnego pulpitu nie odpowiada przez zbyt długi czas. Spróbuj ponownie lub skontaktuj się z administratorem systemu", + "ERROR_UPLOAD_203" : "Serwer zdalnego pulpitu napotkał na błąd podczas transferu. Spróbuj ponownie lub skontaktuj się z administratorem systemu", + "ERROR_UPLOAD_204" : "Docelowa ścieżka transferu pliku nie istnieje. Upewnij się, że ścieżka docelowa istnieje i spróbuj ponownie.", + "ERROR_UPLOAD_205" : "Lokalizacja docelowa dla transferu pliku jest aktualnie zablokowana. Poczekaj na zakończenie trwający zadań i spróbuj ponownie.", + "ERROR_UPLOAD_301" : "Nie masz uprawnień do przesłania tego pliku, ponieważ jest nie zalogowany. Zaloguj się i spróbuj ponownie.", + "ERROR_UPLOAD_303" : "Nie masz uprawnień do przesłania tego pliku. Jeśli potrzebujesz dostępu sprawdź ustawienia systemu lub skontaktuj się z administratorem systemu.", + "ERROR_UPLOAD_308" : "Transfer pliku utknął w martwym punkcie. Zazwyczaj jest to spowodowane niską szybkością i jakością połączenia. Sprawdź swoje ustawienia sieci i spróbuj ponownie.", + "ERROR_UPLOAD_31D" : "Zbyt wiele plików jest aktualnie transferowanych. Poczekaj na zakończenie trwających transferów i spróbuj ponownie.", + "ERROR_UPLOAD_DEFAULT" : "Wystąpił błąd wewnętrzny serwera Guacamole i połączenie zostało zakończone. Jeśli problem nie zniknie, powiadom administratora systemu lub sprawdź log systemu.", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "HELP_CLIPBOARD" : "Tekst skopiowany/wycięty wewnątrz serwera Guacamole pojawi się tutaj. Zmiany dokonane w tym miejscu będą miały wpływ na zawartość zdalnego schowka", + "HELP_INPUT_METHOD_NONE" : "Żadna metoda wejścia nie jest w użyciu. Wejście klawiatury jest akceptowane z podłączonej fizycznej klawiatury.", + "HELP_INPUT_METHOD_OSK" : "Wyświetl i zaakceptuj dane wejściowe z wbudowanej klawiatury ekranowej Guacamole. Klawiatura ekranowa umożliwia wpisywanie kombinacji klawiszy, które w innym przypadku byłyby niemożliwe (takich jak Ctrl-Alt-Del)", + "HELP_INPUT_METHOD_TEXT" : "Zezwalaj na pisanie tekstu i emuluj zdarzenia klawiatury na podstawie wpisanego tekstu. Jest to konieczne w przypadku urządzeń takich jak telefony komórkowe, które nie mają fizycznej klawiatury.", + "HELP_MOUSE_MODE" : "Określa zachowanie zdalnej myszy w odniesieniu do dotknięć.", + "HELP_MOUSE_MODE_ABSOLUTE" : "Dotknij, aby kliknąć. Kliknięcie następuje w miejscu dotknięcia.", + "HELP_MOUSE_MODE_RELATIVE" : "Przeciągnij, aby przesunąć wskaźnik myszy i dotknij, aby kliknąć. Kliknięcie następuje w miejscu, w którym znajduje się wskaźnik.", + "HELP_SHARE_LINK" : "Aktualne połączenie jest udostępniane, dostęp może uzyskać każdy posiadający {LINKS, plural, one{link} other{linki}}:", + + "INFO_CONNECTION_SHARED" : "To połączenie jest teraz udostępniane.", + "INFO_NO_FILE_TRANSFERS" : "Brak transferów plików.", + + "NAME_INPUT_METHOD_NONE" : "Brak", + "NAME_INPUT_METHOD_OSK" : "Klawiatura ekranowa", + "NAME_INPUT_METHOD_TEXT" : "Pole tekstowe", + "NAME_KEY_CTRL" : "Ctrl", + "NAME_KEY_ALT" : "Alt", + "NAME_KEY_ESC" : "Esc", + "NAME_KEY_TAB" : "Tab", + "NAME_MOUSE_MODE_ABSOLUTE" : "Ekran dotykowy", + "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", + + "SECTION_HEADER_CLIPBOARD" : "Schowek", + "SECTION_HEADER_DEVICES" : "Urządzenia", + "SECTION_HEADER_DISPLAY" : "Wyświetlanie", + "SECTION_HEADER_FILE_TRANSFERS" : "Transfer Plików", + "SECTION_HEADER_INPUT_METHOD" : "Metoda wejścia", + "SECTION_HEADER_MOUSE_MODE" : "Tryb emulacji myszy", + + "TEXT_ZOOM_AUTO_FIT" : "Automatycznie dopasuj do okna przeglądarki", + "TEXT_CLIENT_STATUS_IDLE" : "Bezczynne.", + "TEXT_CLIENT_STATUS_CONNECTING" : "Łączenie z Guacamole...", + "TEXT_CLIENT_STATUS_DISCONNECTED" : "Zostałeś rozłączony.", + "TEXT_CLIENT_STATUS_UNSTABLE" : "Połączenie sieciowe z serwerem Guacamole wydaje się być niestabilne.", + "TEXT_CLIENT_STATUS_WAITING" : "Połączono z Guacamole. Czekanie na odpowiedź...", + "TEXT_RECONNECT_COUNTDOWN" : "Ponowne połączenie za {REMAINING} {REMAINING, plural, one{sekundę} other{sekund(-y)}}...", + "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", + + "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.json" + + }, + + "COLOR_SCHEME" : { + + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_HIDE_DETAILS" : "Ukryj", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SHOW_DETAILS" : "Pokaż", + + "FIELD_HEADER_BACKGROUND" : "Drugi plan", + "FIELD_HEADER_FOREGROUND" : "Pierwszy plan", + + "FIELD_OPTION_CUSTOM" : "Niestandardowy...", + + "SECTION_HEADER_DETAILS" : "Szczegóły:" + + }, + + "DATA_SOURCE_DEFAULT" : { + "NAME" : "Domyślny (XML)" + }, + + "FORM" : { + + "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD", + "FIELD_PLACEHOLDER_TIME" : "HH:MM:SS", + + "HELP_SHOW_PASSWORD" : "Naciśnij aby pokazać hasło", + "HELP_HIDE_PASSWORD" : "Naciśnij aby ukryć hasło" + + }, + + "HOME" : { + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_NO_RECENT_CONNECTIONS" : "Brak ostatnich połączeń.", + + "PASSWORD_CHANGED" : "Hasło zmienione.", + + "SECTION_HEADER_ALL_CONNECTIONS" : "Wszystkie połączenia", + "SECTION_HEADER_RECENT_CONNECTIONS" : "Ostatnie połączenia" + + }, + + "LIST" : { + + "TEXT_ANONYMOUS_USER" : "Anonimowy" + + }, + + "LOGIN": { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "ERROR_INVALID_LOGIN" : "Niepoprawny Login", + + "FIELD_HEADER_USERNAME" : "Użytkownik", + "FIELD_HEADER_PASSWORD" : "Hasło" + + }, + + "MANAGE_CONNECTION" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Połączenie", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_LOCATION" : "Lokalizacja:", + "FIELD_HEADER_NAME" : "Nazwa:", + "FIELD_HEADER_PROTOCOL" : "Protokół:", + + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + + "INFO_CONNECTION_DURATION_UNKNOWN" : "--", + "INFO_CONNECTION_ACTIVE_NOW" : "Aktywne Teraz", + "INFO_CONNECTION_NOT_USED" : "To połączenie nie zostało jeszcze użyte.", + + "SECTION_HEADER_EDIT_CONNECTION" : "Edytuj Połączenie", + "SECTION_HEADER_HISTORY" : "Historia Użycia", + "SECTION_HEADER_PARAMETERS" : "Parametry", + + "TABLE_HEADER_HISTORY_USERNAME" : "Użytkownik", + "TABLE_HEADER_HISTORY_START" : "Czas Rozpoczęcia", + "TABLE_HEADER_HISTORY_DURATION" : "Czas Trwania", + "TABLE_HEADER_HISTORY_REMOTEHOST" : "Zdalny Host", + + "TEXT_CONFIRM_DELETE" : "Połączenia nie mogą zostać przywrócone po ich usunięciu. Czy na pewno chcesz usunąć to połączenie?", + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + + }, + + "MANAGE_CONNECTION_GROUP" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Grupę Połączeń", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_LOCATION" : "Lokalizacja:", + "FIELD_HEADER_NAME" : "Nazwa:", + "FIELD_HEADER_TYPE" : "Typ:", + + "NAME_TYPE_BALANCING" : "Równoważący obciążenie", + "NAME_TYPE_ORGANIZATIONAL" : "Organizacyjny", + + "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Edytuj Grupę Połączeń", + + "TEXT_CONFIRM_DELETE" : "Grupy połączeń nie mogą zostać przywrócone po tym jak zostaną usunięte. Czy na pewno chcesz usunąć tę grupę połączeń?" + + }, + + "MANAGE_SHARING_PROFILE" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Profil Udostępniania", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_NAME" : "Nazwa:", + "FIELD_HEADER_PRIMARY_CONNECTION" : "Podstawowe Połączenie:", + + "SECTION_HEADER_EDIT_SHARING_PROFILE" : "Edytuj Profil Udostępniania", + "SECTION_HEADER_PARAMETERS" : "Parametry", + + "TEXT_CONFIRM_DELETE" : "Profile udostępniania nie mogą zostać przywrócone po tym jak zostaną usunięte. Czy na pewno chcesz usunąć ten profil udostępniania?" + + }, + + "MANAGE_USER" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLONE" : "@:APP.ACTION_CLONE", + "ACTION_DELETE" : "@:APP.ACTION_DELETE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + + "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Użytkownika", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administracja systemem:", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Zmiana własnego hasła:", + "FIELD_HEADER_CREATE_NEW_USERS" : "Tworzenie nowych użytkowników:", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "Tworzenie nowych grup użytkowników:", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Tworzenie nowych połączeń:", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Tworzenie nowych grup połączeń:", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Tworzenie nowych profili udostępniania:", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_USERNAME" : "Nazwa użytkownika:", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "HELP_NO_USER_GROUPS" : "Ten użytkownik nie należy obecnie do żadnej grupy. Rozwiń tę sekcje aby dodać do grupy.", + + "INFO_READ_ONLY" : "Ten użytkownik nie może być edytowany", + "INFO_NO_USER_GROUPS_AVAILABLE" : "Brak dostępnych grup.", + + "SECTION_HEADER_ALL_CONNECTIONS" : "Wszystkie Połączenia", + "SECTION_HEADER_CONNECTIONS" : "Połączenia", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "Obecne Połączenia", + "SECTION_HEADER_EDIT_USER" : "Edytuj Użytkownika", + "SECTION_HEADER_PERMISSIONS" : "Uprawnienia", + "SECTION_HEADER_USER_GROUPS" : "Grupy", + + "TEXT_CONFIRM_DELETE" : "Użytkownicy nie mogą zostać przywróceni po tym jak zostaną usunięci. Czy na pewno chcesz usunąć tego użytkownika?" + + }, + + "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" : "Usuń grupę", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "FIELD_HEADER_USER_GROUP_NAME" : "Nazwa grupy:", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "HELP_NO_USER_GROUPS" : "To grupa nie należy do żadnej grupy. Rozwiń tę sekcje aby dodać do grupy.", + "HELP_NO_MEMBER_USER_GROUPS" : "Ta grupa nie zawiera żadnych grup. Rozwiń tę sekcje aby dodać grupy.", + "HELP_NO_MEMBER_USERS" : "Ta grupa nie zawiera żadnych użytkowników. Rozwiń tę sekcje aby dodać użytkowników do grupy.", + + "INFO_READ_ONLY" : "Ta grupa nie może być edytowana", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USERS_AVAILABLE" : "Brak dostępnych użytkowników.", + + "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" : "Edytuj Grupę", + "SECTION_HEADER_MEMBER_USERS" : "Należący użytkownicy", + "SECTION_HEADER_MEMBER_USER_GROUPS" : "Należące grupy", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_USER_GROUPS" : "Grupy nadrzędne", + + "TEXT_CONFIRM_DELETE" : "Grupy nie mogą zostać przywrócone po tym jak zostaną usunięte. Czy na pewno chcesz usunąć tę grupę?" + + }, + + "PLAYER" : { + + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", + "ACTION_PLAY" : "@:APP.ACTION_PLAY", + + "INFO_LOADING_RECORDING" : "Twoje nagranie jest właśnie ładowane. Proszę czekać...", + "INFO_SEEK_IN_PROGRESS" : "Szukanie żądanej pozycji. Proszę czekać..." + + }, + + "PROTOCOL_KUBERNETES" : { + + "FIELD_HEADER_BACKSPACE" : "Klawisz Backspace wysyła:", + "FIELD_HEADER_CA_CERT" : "Certyfikat CA:", + "FIELD_HEADER_CLIENT_CERT" : "Certyfikat klienta:", + "FIELD_HEADER_CLIENT_KEY" : "Klucz klienta:", + "FIELD_HEADER_COLOR_SCHEME" : "Schemat kolorów:", + "FIELD_HEADER_CONTAINER" : "Nazwa kontenera:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatycznie utwórz ścieżkę nagrywania:", + "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatycznie utwórz ścieżkę maszynopisu:", + "FIELD_HEADER_EXEC_COMMAND" : "Komenda (exec):", + "FIELD_HEADER_FONT_NAME" : "Nazwa czcionki:", + "FIELD_HEADER_FONT_SIZE" : "Rozmiar czcionki:", + "FIELD_HEADER_HOSTNAME" : "Nazwa Hosta:", + "FIELD_HEADER_IGNORE_CERT" : "Ignoruj certyfikat serwera:", + "FIELD_HEADER_NAMESPACE" : "Przestrzeń nazw:", + "FIELD_HEADER_POD" : "Nazwa pod'a:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_READ_ONLY" : "Tylko do odczytu:", + "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Wyklucz mysz:", + "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Wyklucz grafiki/strumienie:", + "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Dołącz zdarzenia klawiszy:", + "FIELD_HEADER_RECORDING_NAME" : "Nazwa nagrania:", + "FIELD_HEADER_RECORDING_PATH" : "Ścieżka nagrania:", + "FIELD_HEADER_SCROLLBACK" : "Maksymalny rozmiar przewijania:", + "FIELD_HEADER_TYPESCRIPT_NAME" : "Nazwa maszynopisu:", + "FIELD_HEADER_TYPESCRIPT_PATH" : "Ścieżka maszynopisu:", + "FIELD_HEADER_USE_SSL" : "Użyj SSL/TLS", + + "FIELD_OPTION_BACKSPACE_EMPTY" : "", + "FIELD_OPTION_BACKSPACE_8" : "Backspace (Ctrl-H)", + "FIELD_OPTION_BACKSPACE_127" : "Delete (Ctrl-?)", + + "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Czarny na białym", + "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", + "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Szary na czarnym", + "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Zielony na czarnym", + "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Biały na czarnym", + + "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" : "Uwierzytelnianie", + "SECTION_HEADER_BEHAVIOR" : "Zachowanie terminala", + "SECTION_HEADER_CONTAINER" : "Kontener", + "SECTION_HEADER_DISPLAY" : "Wyświetlanie", + "SECTION_HEADER_RECORDING" : "Nagrywanie Ekranu", + "SECTION_HEADER_TYPESCRIPT" : "Maszynopis (Tekstowe nagranie sesji)", + "SECTION_HEADER_NETWORK" : "Sieć" + + }, + + "PROTOCOL_RDP" : { + + "FIELD_HEADER_CLIENT_NAME" : "Nazwa klienta:", + "FIELD_HEADER_COLOR_DEPTH" : "Głębia kolorów:", + "FIELD_HEADER_CONSOLE" : "Konsola administratora:", + "FIELD_HEADER_CONSOLE_AUDIO" : "Wspieraj dźwięk w konsoli:", + "FIELD_HEADER_CREATE_DRIVE_PATH" : "Automatycznie utwórz dysk:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatycznie utwórz ścieżkę nagrywania:", + "FIELD_HEADER_DISABLE_AUDIO" : "Wyłącz dźwięk:", + "FIELD_HEADER_DISABLE_AUTH" : "Wyłącz uwierzytelnianie:", + "FIELD_HEADER_DISABLE_COPY" : "Wyłącz kopiowanie ze zdalnego pulpitu:", + "FIELD_HEADER_DISABLE_DOWNLOAD" : "Wyłącz pobieranie plików:", + "FIELD_HEADER_DISABLE_PASTE" : "Wyłącz wklejanie z klienta:", + "FIELD_HEADER_DISABLE_UPLOAD" : "Wyłącz przesyłanie plików:", + "FIELD_HEADER_DOMAIN" : "Domena:", + "FIELD_HEADER_DPI" : "Rozdzielczość (DPI):", + "FIELD_HEADER_DRIVE_NAME" : "Nazwa dysku:", + "FIELD_HEADER_DRIVE_PATH" : "Ścieżka dysku:", + "FIELD_HEADER_ENABLE_AUDIO_INPUT" : "Włącz wejście dźwięku (mikrofon):", + "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Włącz kompozycje pulpitu (Aero):", + "FIELD_HEADER_ENABLE_DRIVE" : "Włącz dysk:", + "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Włącz wygładzanie czcionek (ClearType):", + "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Włącz przeciąganie w pełnym oknie:", + "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Włącz animacje menu:", + "FIELD_HEADER_DISABLE_BITMAP_CACHING" : "Wyłącz bitmap caching:", + "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING" : "Wyłącz off-screen caching:", + "FIELD_HEADER_DISABLE_GLYPH_CACHING" : "Wyłącz glyph caching:", + "FIELD_HEADER_DISABLE_GFX" : "Wyłącz Rozszerzenie Potoku Graficznego:", + "FIELD_HEADER_ENABLE_PRINTING" : "Włącz drukowanie:", + "FIELD_HEADER_ENABLE_SFTP" : "Włącz SFTP:", + "FIELD_HEADER_ENABLE_THEMING" : "Włącz motyw:", + "FIELD_HEADER_ENABLE_TOUCH" : "Włącz multi-touch:", + "FIELD_HEADER_ENABLE_WALLPAPER" : "Włącz tapetę:", + "FIELD_HEADER_FORCE_LOSSLESS" : "Wymuś bezstratną kompresję:", + "FIELD_HEADER_GATEWAY_DOMAIN" : "Domena:", + "FIELD_HEADER_GATEWAY_HOSTNAME" : "Nazwa hosta:", + "FIELD_HEADER_GATEWAY_PASSWORD" : "Hasło:", + "FIELD_HEADER_GATEWAY_PORT" : "Port:", + "FIELD_HEADER_GATEWAY_USERNAME" : "Użytkownik:", + "FIELD_HEADER_HEIGHT" : "Wysokość:", + "FIELD_HEADER_HOSTNAME" : "Nazwa hosta:", + "FIELD_HEADER_IGNORE_CERT" : "Ignoruj certyfikat serwera:", + "FIELD_HEADER_INITIAL_PROGRAM" : "Program początkowy:", + "FIELD_HEADER_LOAD_BALANCE_INFO" : "Load balance info/cookie:", + "FIELD_HEADER_NORMALIZE_CLIPBOARD" : "Zakończenie linii:", + "FIELD_HEADER_PASSWORD" : "Hasło:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_PRINTER_NAME" : "Nazwa przekierowanej drukarki:", + "FIELD_HEADER_PRECONNECTION_BLOB" : "Preconnection BLOB (VM ID):", + "FIELD_HEADER_PRECONNECTION_ID" : "Źródłowe ID RDP:", + "FIELD_HEADER_READ_ONLY" : "Tylko do odczytu:", + "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Wyklucz mysz:", + "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Wyklucz grafiki/strumienie:", + "FIELD_HEADER_RECORDING_EXCLUDE_TOUCH" : "Wyklucz zdarzenia dotykowe:", + "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Dołącz zdarzenia klawiszy:", + "FIELD_HEADER_RECORDING_NAME" : "Nazwa nagrania:", + "FIELD_HEADER_RECORDING_PATH" : "Ścieżka nagrania:", + "FIELD_HEADER_RESIZE_METHOD" : "Metoda zmiany rozmiaru:", + "FIELD_HEADER_REMOTE_APP_ARGS" : "Parametry:", + "FIELD_HEADER_REMOTE_APP_DIR" : "Katalog roboczy:", + "FIELD_HEADER_REMOTE_APP" : "Program:", + "FIELD_HEADER_SECURITY" : "Tryb bezpieczeństwa:", + "FIELD_HEADER_SERVER_LAYOUT" : "Układ klawiatury:", + "FIELD_HEADER_SFTP_DIRECTORY" : "Domyślny katalog dla przesyłania:", + "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Wyłącz pobieranie plików:", + "FIELD_HEADER_SFTP_HOST_KEY" : "Publiczny klucz hosta (Base64):", + "FIELD_HEADER_SFTP_HOSTNAME" : "Nazwa Hosta:", + "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Interwał utrzymania aktywności (keepalive) SFTP:", + "FIELD_HEADER_SFTP_PASSPHRASE" : "Hasło klucza:", + "FIELD_HEADER_SFTP_PASSWORD" : "Hasło:", + "FIELD_HEADER_SFTP_PORT" : "Port:", + "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Klucz prywatny:", + "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Katalog nadrzędny przeglądarki plików:", + "FIELD_HEADER_SFTP_DISABLE_UPLOAD" : "Wyłącz przesyłanie plików:", + "FIELD_HEADER_SFTP_USERNAME" : "Użytkownik:", + "FIELD_HEADER_STATIC_CHANNELS" : "Nazwy statycznych kanałów:", + "FIELD_HEADER_TIMEZONE" : "Strefa czasowa:", + "FIELD_HEADER_USERNAME" : "Użytkownik:", + "FIELD_HEADER_WIDTH" : "Szerokość:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adres rozgłoszeniowy (broadcast) dla pakietu WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "Adres MAC zdalnego hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Wyślij pakiet WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "Port UDP dla pakietu WoL:", + "FIELD_HEADER_WOL_WAIT_TIME" : "Czas oczekiwania na rozruch hosta:", + + "FIELD_OPTION_NORMALIZE_CLIPBOARD_EMPTY" : "", + "FIELD_OPTION_NORMALIZE_CLIPBOARD_PRESERVE" : "Zachowaj tak, jak jest", + "FIELD_OPTION_NORMALIZE_CLIPBOARD_UNIX" : "Linux/Mac/Unix (LF)", + "FIELD_OPTION_NORMALIZE_CLIPBOARD_WINDOWS" : "Windows (CRLF)", + + "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", + "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", + "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-bit)", + "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", + "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", + + "FIELD_OPTION_RESIZE_METHOD_DISPLAY_UPDATE" : "\"Aktualizacja Wyświetlania\" kanał wirtualny (RDP 8.1+)", + "FIELD_OPTION_RESIZE_METHOD_EMPTY" : "", + "FIELD_OPTION_RESIZE_METHOD_RECONNECT" : "Połącz Ponownie", + + "FIELD_OPTION_SECURITY_ANY" : "Dowolny", + "FIELD_OPTION_SECURITY_EMPTY" : "", + "FIELD_OPTION_SECURITY_NLA" : "NLA (Uwierzytelnianie Na Poziomie Sieci)", + "FIELD_OPTION_SECURITY_RDP" : "szyfrowanie RDP", + "FIELD_OPTION_SECURITY_TLS" : "szyfrowanie TLS", + "FIELD_OPTION_SECURITY_VMCONNECT" : "Hyper-V / VMConnect", + + "FIELD_OPTION_SERVER_LAYOUT_DE_CH_QWERTZ" : "Szwajcarski Niemiecki (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "Niemiecki (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_EMPTY" : "", + "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK Angielski (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US Angielski (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_ES_ES_QWERTY" : "Hiszpański (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_ES_LATAM_QWERTY" : "Latynoamerykański (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", + "FIELD_OPTION_SERVER_LAYOUT_FR_BE_AZERTY" : "Belgijski Francuski (Azerty)", + "FIELD_OPTION_SERVER_LAYOUT_FR_CA_QWERTY" : "Kanadyjski Francuski (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Szwajcarski Francuski (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Francuski (Azerty)", + "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Węgierski (Qwertz)", + "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Włoski (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japoński (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_NO_NO_QWERTY" : "Norweski (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_PL_PL_QWERTY" : "Polski (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portugalski Brazylijski (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Szwecki (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Duński (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turecki-Q (Qwerty)", + + "NAME" : "RDP", + + "SECTION_HEADER_AUTHENTICATION" : "Uwierzytelnianie", + "SECTION_HEADER_BASIC_PARAMETERS" : "Ustawienia Podstawowe", + "SECTION_HEADER_CLIPBOARD" : "Schowek", + "SECTION_HEADER_DEVICE_REDIRECTION" : "Przekierowanie Urządzeń", + "SECTION_HEADER_DISPLAY" : "Wyświetlanie", + "SECTION_HEADER_GATEWAY" : "Remote Desktop Gateway", + "SECTION_HEADER_LOAD_BALANCING" : "Load Balancing", + "SECTION_HEADER_NETWORK" : "Sieć", + "SECTION_HEADER_PERFORMANCE" : "Wydajność", + "SECTION_HEADER_PRECONNECTION_PDU" : "Wstępne połączenie PDU / Hyper-V", + "SECTION_HEADER_RECORDING" : "Nagrywanie Ekranu", + "SECTION_HEADER_REMOTEAPP" : "RemoteApp", + "SECTION_HEADER_SFTP" : "SFTP", + "SECTION_HEADER_WOL" : "Wake-on-LAN (WoL)" + + }, + + "PROTOCOL_SSH" : { + + "FIELD_HEADER_BACKSPACE" : "Klawisz Backspace wysyła:", + "FIELD_HEADER_COLOR_SCHEME" : "Schemat kolorów:", + "FIELD_HEADER_COMMAND" : "Wykonaj polecenie:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatycznie utwórz ścieżkę nagrywania:", + "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatycznie utwórz ścieżkę maszynopisu:", + "FIELD_HEADER_DISABLE_COPY" : "Wyłącz kopiowanie z terminala:", + "FIELD_HEADER_DISABLE_PASTE" : "Wyłącz wklejanie z klienta:", + "FIELD_HEADER_FONT_NAME" : "Nazwa czcionki:", + "FIELD_HEADER_FONT_SIZE" : "Rozmiar czcionki:", + "FIELD_HEADER_ENABLE_SFTP" : "Włącz SFTP:", + "FIELD_HEADER_HOST_KEY" : "Publiczny klucz hosta (Base64):", + "FIELD_HEADER_HOSTNAME" : "Nazwa Hosta:", + "FIELD_HEADER_LOCALE" : "Język/Ustawienia regionalne ($LANG):", + "FIELD_HEADER_USERNAME" : "Użytkownik:", + "FIELD_HEADER_PASSWORD" : "Hasło:", + "FIELD_HEADER_PASSPHRASE" : "Hasło klucza:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_PRIVATE_KEY" : "Klucz prywatny:", + "FIELD_HEADER_SCROLLBACK" : "Maksymalny rozmiar przewijania:", + "FIELD_HEADER_READ_ONLY" : "Tylko do odczytu:", + "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Wyklucz mysz:", + "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Wyklucz grafiki/strumienie:", + "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Dołącz zdarzenia klawiszy:", + "FIELD_HEADER_RECORDING_NAME" : "Nazwa nagrania:", + "FIELD_HEADER_RECORDING_PATH" : "Ścieżka nagrania:", + "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "Interwał utrzymywania aktywności serwera (keepalive):", + "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Wyłącz pobieranie plików:", + "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Katalog nadrzędny przeglądarki plików:", + "FIELD_HEADER_SFTP_DISABLE_UPLOAD" : "Wyłącz przesyłanie plików:", + "FIELD_HEADER_TERMINAL_TYPE" : "Typ terminala:", + "FIELD_HEADER_TIMEZONE" : "Strefa czasowa ($TZ):", + "FIELD_HEADER_TYPESCRIPT_NAME" : "Nazwa maszynopisu:", + "FIELD_HEADER_TYPESCRIPT_PATH" : "Ścieżka maszynopisu:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adres rozgłoszeniowy (broadcast) dla pakietu WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "Adres MAC zdalnego hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Wyślij pakiet WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "Port UDP dla pakietu WoL:", + "FIELD_HEADER_WOL_WAIT_TIME" : "Czas oczekiwania na rozruch hosta:", + + "FIELD_OPTION_BACKSPACE_EMPTY" : "", + "FIELD_OPTION_BACKSPACE_8" : "Backspace (Ctrl-H)", + "FIELD_OPTION_BACKSPACE_127" : "Delete (Ctrl-?)", + + "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Czarny na białym", + "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", + "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Szary na czarnym", + "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Zielony na czarnym", + "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Biały na czarnym", + + "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" : "", + + "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" : "Uwierzytelnianie", + "SECTION_HEADER_BEHAVIOR" : "Zachowanie Terminala", + "SECTION_HEADER_CLIPBOARD" : "Schowek", + "SECTION_HEADER_DISPLAY" : "Wyświetlanie", + "SECTION_HEADER_NETWORK" : "Sieć", + "SECTION_HEADER_RECORDING" : "Nagrywanie Ekranu", + "SECTION_HEADER_SESSION" : "Sesja / Środowisko", + "SECTION_HEADER_TYPESCRIPT" : "Maszynopis (Tekstowe Nagranie Sesji)", + "SECTION_HEADER_SFTP" : "SFTP", + "SECTION_HEADER_WOL" : "Wake-on-LAN (WoL)" + + }, + + "PROTOCOL_TELNET" : { + + "FIELD_HEADER_BACKSPACE" : "Klawisz Backspace wysyła:", + "FIELD_HEADER_COLOR_SCHEME" : "Schemat kolorów:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatycznie utwórz ścieżkę nagrywania:", + "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatycznie utwórz ścieżkę maszynopisu:", + "FIELD_HEADER_DISABLE_COPY" : "Wyłącz kopiowanie z terminala:", + "FIELD_HEADER_DISABLE_PASTE" : "Wyłącz wklejanie z klienta:", + "FIELD_HEADER_FONT_NAME" : "Nazwa czcionki:", + "FIELD_HEADER_FONT_SIZE" : "Rozmiar czcionki:", + "FIELD_HEADER_HOSTNAME" : "Nazwa Hosta:", + "FIELD_HEADER_LOGIN_FAILURE_REGEX" : "Wyrażenie regularne nieudanego logowania:", + "FIELD_HEADER_LOGIN_SUCCESS_REGEX" : "Wyrażenie regularne udanego logowania:", + "FIELD_HEADER_USERNAME" : "Użytkownik:", + "FIELD_HEADER_USERNAME_REGEX" : "Wyrażenie regularne użytkownika:", + "FIELD_HEADER_PASSWORD" : "Hasło:", + "FIELD_HEADER_PASSWORD_REGEX" : "Wyrażenie regularne hasła", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_READ_ONLY" : "Tylko do odczytu:", + "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Wyklucz mysz:", + "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Wyklucz grafiki/strumienie:", + "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Dołącz wciśnięcia klawiszy:", + "FIELD_HEADER_RECORDING_NAME" : "Nazwa nagrania:", + "FIELD_HEADER_RECORDING_PATH" : "Ścieżka nagrania:", + "FIELD_HEADER_SCROLLBACK" : "Maksymalny rozmiar przewijania:", + "FIELD_HEADER_TERMINAL_TYPE" : "Typ terminala:", + "FIELD_HEADER_TYPESCRIPT_NAME" : "Nazwa maszynopisu:", + "FIELD_HEADER_TYPESCRIPT_PATH" : "Ścieżka maszynopisu:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adres rozgłoszeniowy (broadcast) dla pakietu WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "Adres MAC zdalnego hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Wyślij pakiet WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "Port UDP dla pakietu WoL:", + "FIELD_HEADER_WOL_WAIT_TIME" : "Czas oczekiwania na rozruch hosta:", + + "FIELD_OPTION_BACKSPACE_EMPTY" : "", + "FIELD_OPTION_BACKSPACE_8" : "Backspace (Ctrl-H)", + "FIELD_OPTION_BACKSPACE_127" : "Delete (Ctrl-?)", + + "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Czarny na białym", + "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", + "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Szary na czarnym", + "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Zielony na czarnym", + "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Biały na czarnym", + + "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" : "", + + "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" : "Uwierzytelnianie", + "SECTION_HEADER_BEHAVIOR" : "Zachowanie Terminala", + "SECTION_HEADER_CLIPBOARD" : "Schowek", + "SECTION_HEADER_DISPLAY" : "Wyświetlanie", + "SECTION_HEADER_RECORDING" : "Nagrywanie Ekranu", + "SECTION_HEADER_TYPESCRIPT" : "Maszynopis (Tekstowe Nagranie Sesji)", + "SECTION_HEADER_NETWORK" : "Sieć", + "SECTION_HEADER_WOL" : "Wake-on-LAN (WoL)" + + }, + + "PROTOCOL_VNC" : { + + "FIELD_HEADER_AUDIO_SERVERNAME" : "Nazwa serwera audio:", + "FIELD_HEADER_CLIPBOARD_ENCODING" : "Kodowanie:", + "FIELD_HEADER_COLOR_DEPTH" : "Głębia kolorów:", + "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatycznie utwórz ścieżkę nagrywania:", + "FIELD_HEADER_CURSOR" : "Kursor:", + "FIELD_HEADER_DEST_HOST" : "Docelowy host:", + "FIELD_HEADER_DEST_PORT" : "Docelowy port:", + "FIELD_HEADER_DISABLE_COPY" : "Wyłącz kopiowanie ze zdalnego pulpitu:", + "FIELD_HEADER_DISABLE_PASTE" : "Wyłącz wklejanie z klienta:", + "FIELD_HEADER_ENABLE_AUDIO" : "Włącz dźwięk:", + "FIELD_HEADER_ENABLE_SFTP" : "Włącz SFTP:", + "FIELD_HEADER_FORCE_LOSSLESS" : "Wymuś bezstratną kompresję:", + "FIELD_HEADER_HOSTNAME" : "Nazwa Hosta:", + "FIELD_HEADER_USERNAME" : "Użytkownik:", + "FIELD_HEADER_PASSWORD" : "Hasło:", + "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_READ_ONLY" : "Tylko do odczytu:", + "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Wyklucz mysz:", + "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Wyklucz grafiki/strumienie:", + "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Dołącz zdarzenia klawiszy:", + "FIELD_HEADER_RECORDING_NAME" : "Nazwa nagrania:", + "FIELD_HEADER_RECORDING_PATH" : "Ścieżka nagrania:", + "FIELD_HEADER_SFTP_DIRECTORY" : "Domyślny katalog przesyłania:", + "FIELD_HEADER_SFTP_DISABLE_DOWNLOAD" : "Wyłącz pobieranie plików:", + "FIELD_HEADER_SFTP_HOST_KEY" : "Publiczny klucz hosta (Base64):", + "FIELD_HEADER_SFTP_HOSTNAME" : "Nazwa Hosta:", + "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "Interwał utrzymania aktywności (keepalive) SFTP:", + "FIELD_HEADER_SFTP_PASSPHRASE" : "Hasło klucza:", + "FIELD_HEADER_SFTP_PASSWORD" : "Hasło:", + "FIELD_HEADER_SFTP_PORT" : "Port:", + "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Klucz prywatny:", + "FIELD_HEADER_SFTP_ROOT_DIRECTORY" : "Katalog nadrzędny przeglądarki plików:", + "FIELD_HEADER_SFTP_DISABLE_UPLOAD" : "Wyłącz przesyłanie plików:", + "FIELD_HEADER_SFTP_USERNAME" : "Użytkownik:", + "FIELD_HEADER_SWAP_RED_BLUE" : "Zamień czerwone/niebieskie komponenty:", + "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adres rozgłoszeniowy (broadcast) dla pakietu WoL:", + "FIELD_HEADER_WOL_MAC_ADDR" : "Adres MAC zdalnego hosta:", + "FIELD_HEADER_WOL_SEND_PACKET" : "Wyślij pakiet WoL:", + "FIELD_HEADER_WOL_UDP_PORT" : "Port UDP dla pakietu WoL:", + "FIELD_HEADER_WOL_WAIT_TIME" : "Czas oczekiwania na rozruch hosta:", + + "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", + "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", + "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", + "FIELD_OPTION_COLOR_DEPTH_32" : "True color (32-bit)", + "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", + + "FIELD_OPTION_CURSOR_EMPTY" : "", + "FIELD_OPTION_CURSOR_LOCAL" : "Lokalny", + "FIELD_OPTION_CURSOR_REMOTE" : "Zdalny", + + "FIELD_OPTION_CLIPBOARD_ENCODING_CP1252" : "CP1252", + "FIELD_OPTION_CLIPBOARD_ENCODING_EMPTY" : "", + "FIELD_OPTION_CLIPBOARD_ENCODING_ISO8859_1" : "ISO 8859-1", + "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_8" : "UTF-8", + "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_16" : "UTF-16", + + "NAME" : "VNC", + + "SECTION_HEADER_AUDIO" : "Dźwięk", + "SECTION_HEADER_AUTHENTICATION" : "Uwierzytelnianie", + "SECTION_HEADER_CLIPBOARD" : "Schowek", + "SECTION_HEADER_DISPLAY" : "Wyświetlanie", + "SECTION_HEADER_NETWORK" : "Sieć", + "SECTION_HEADER_RECORDING" : "Nagrywanie Ekranu", + "SECTION_HEADER_REPEATER" : "Przekaźnik VNC", + "SECTION_HEADER_SFTP" : "SFTP", + "SECTION_HEADER_WOL" : "Wake-on-LAN (WoL)" + + }, + + "SETTINGS" : { + + "SECTION_HEADER_SETTINGS" : "Ustawienia" + + }, + + "SETTINGS_CONNECTION_HISTORY" : { + + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "FILENAME_HISTORY_CSV" : "history.csv", + + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + + "HELP_CONNECTION_HISTORY" : "Rekordy historii poprzednich połączeń są wylistowane tutaj i można je sortować klikając nagłówki kolumn. Aby wyszukać określone rekordy wprowadź ciąg filtru i kliknij \"Szukaj\". Tylko rekordy pasujące do podanego filtru zostaną wyświetlone.", + + "INFO_CONNECTION_DURATION_UNKNOWN" : "--", + "INFO_NO_HISTORY" : "Brak pasujących rekordów", + + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nazwa połączenia", + "TABLE_HEADER_SESSION_DURATION" : "Czas trwania", + "TABLE_HEADER_SESSION_LOGS" : "Logi", + "TABLE_HEADER_SESSION_REMOTEHOST" : "Zdalny host", + "TABLE_HEADER_SESSION_STARTDATE" : "Czas rozpoczęcia", + "TABLE_HEADER_SESSION_USERNAME" : "Użytkownik", + + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + + }, + + "SETTINGS_CONNECTIONS" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_NEW_CONNECTION" : "Nowe Połączenie", + "ACTION_NEW_CONNECTION_GROUP" : "Nowa Grupa", + "ACTION_NEW_SHARING_PROFILE" : "Nowy Profil Udostępniania", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "HELP_CONNECTIONS" : "Kliknij lub naciśnij połączenie poniżej, aby nim zarządzać. W zależności od poziomu dostępu, połączenia mogą być dodawane i usuwane, a ich właściwości (protokół, nazwa hosta, port itp.) mogą być zmieniane.", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "SECTION_HEADER_CONNECTIONS" : "Połączenia" + + }, + + "SETTINGS_PREFERENCES" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + + "FIELD_HEADER_LANGUAGE" : "Język:", + "FIELD_HEADER_PASSWORD" : "Hasło:", + "FIELD_HEADER_PASSWORD_OLD" : "Obecne Hasło:", + "FIELD_HEADER_PASSWORD_NEW" : "Nowe Hasło:", + "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Powtórz Nowe Hasło:", + "FIELD_HEADER_TIMEZONE" : "Strefa Czasowa:", + "FIELD_HEADER_USERNAME" : "Użytkownik:", + + "HELP_DEFAULT_INPUT_METHOD" : "Domyślna metoda wprowadzania określa sposób odbierania zdarzeń klawiatury przez Guacamole. Zmiana tego ustawienia może być konieczna w przypadku korzystania z urządzenia mobilnego lub pisania w edytorze IME. To ustawienie można zmienić dla każdego połączenia w menu Guacamole.", + "HELP_DEFAULT_MOUSE_MODE" : "Domyślny tryb emulacji myszy określa, jak zdalna mysz będzie się zachowywać w nowych połączeniach w odniesieniu do dotknięć. To ustawienie można zmienić dla każdego połączenia w menu Guacamole.", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_LOCALE" : "Poniższe opcje są związane z ustawieniami regionalnymi użytkownika i wpłyną na sposób wyświetlania różnych części interfejsu", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_UPDATE_PASSWORD" : "Jeśli chcesz zmienić swoje hasło, wprowadź poniżej obecne hasło i żądane nowe hasło, a następnie kliknij \"Zmień Hasło\". Zmiana będzie miała efekt natychmiastowy.", + + "INFO_PASSWORD_CHANGED" : "Hasło zmienione.", + "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "Ustawienia użytkownika zapisane.", + + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + + "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Domyślna Metoda Wprowadzania", + "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Domyślny Tryb Emulacji Myszy", + "SECTION_HEADER_UPDATE_PASSWORD" : "Zmień Hasło" + + }, + + "SETTINGS_USERS" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_NEW_USER" : "Nowy Użytkownik", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + + "HELP_USERS" : "Kliknij lub dotknij użytkownika poniżej, aby nim zarządzać. W zależności od Twojego poziomu dostępu, użytkowników można dodawać i usuwać, a także zmieniać ich hasła.", + + "SECTION_HEADER_USERS" : "Użytkownicy", + + "TABLE_HEADER_FULL_NAME" : "Imię i Nazwisko", + "TABLE_HEADER_LAST_ACTIVE" : "Ostatnio aktywny", + "TABLE_HEADER_ORGANIZATION" : "Organizacja", + "TABLE_HEADER_USERNAME" : "Nazwa użytkownika" + + }, + + "SETTINGS_USER_GROUPS" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_NEW_USER_GROUP" : "Nowa Grupa", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + + "HELP_USER_GROUPS" : "Kliknij lub naciśnij grupę poniżej, aby nią zarządzać. W zależności od Twojego poziomu dostępu, grupy mogą być dodawane i usuwane, a ich użytkownicy i grupy mogą być edytowane.", + + "SECTION_HEADER_USER_GROUPS" : "Grupy", + + "TABLE_HEADER_USER_GROUP_NAME" : "Nazwa Grupy" + + }, + + "SETTINGS_SESSIONS" : { + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_DELETE" : "Zakończ sesję", + + "DIALOG_HEADER_CONFIRM_DELETE" : "Zakończ Sesję", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + + "HELP_SESSIONS" : "Ta strona zostanie wypełniona aktualnie aktywnymi połączeniami. Lista połączeń i możliwość ich zakończenia zależy od Twojego poziomu dostępu. Jeśli chcesz zakończyć jedną lub więcej sesji, zaznacz pola obok tych sesji i kliknij \"Zakończ sesję\". Zakończenie sesji spowoduje natychmiastowe rozłączenie użytkownika z powiązanym połączeniem.", + + + "INFO_NO_SESSIONS" : "Brak aktywnych połączeń", + + "SECTION_HEADER_SESSIONS" : "Aktywne Połączenia", + + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nazwa połączenia", + "TABLE_HEADER_SESSION_REMOTEHOST" : "Zdalny host", + "TABLE_HEADER_SESSION_STARTDATE" : "Aktywne od", + "TABLE_HEADER_SESSION_USERNAME" : "Użytkownik", + + "TEXT_CONFIRM_DELETE" : "Czy na pewno chcesz zakończyć wszystkie wybrane sesje? Użytkownicy korzystający z tych sesji zostaną natychmiast rozłączeni." + + }, + + "USER_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_EMAIL_ADDRESS" : "Adres email:", + "FIELD_HEADER_GUAC_FULL_NAME" : "Imię i Nazwisko:", + "FIELD_HEADER_GUAC_ORGANIZATION" : "Organizacja:", + "FIELD_HEADER_GUAC_ORGANIZATIONAL_ROLE" : "Rola:" + + }, + + "USER_MENU" : { + + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + + } + +} \ No newline at end of file From 56b4cc436d4dbc7cb426e07bd4ff03d8136d3908 Mon Sep 17 00:00:00 2001 From: Mike Jumper Date: Tue, 10 Jan 2023 23:30:47 -0800 Subject: [PATCH 104/289] GUACAMOLE-1731: Correct parent project version (bumped to 1.5.0). --- extensions/guacamole-auth-ban/pom.xml | 6 +++--- .../src/main/resources/guac-manifest.json | 2 +- extensions/guacamole-display-statistics/pom.xml | 4 ++-- .../src/main/resources/guac-manifest.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/guacamole-auth-ban/pom.xml b/extensions/guacamole-auth-ban/pom.xml index 68b8d6f90..fcc2a272d 100644 --- a/extensions/guacamole-auth-ban/pom.xml +++ b/extensions/guacamole-auth-ban/pom.xml @@ -26,14 +26,14 @@ org.apache.guacamole guacamole-auth-ban jar - 1.4.0 + 1.5.0 guacamole-auth-ban http://guacamole.apache.org/ org.apache.guacamole extensions - 1.4.0 + 1.5.0 ../ @@ -51,7 +51,7 @@ org.apache.guacamole guacamole-ext - 1.4.0 + 1.5.0 provided + + + 4.0.0 + org.apache.guacamole + guacamole-auth-sso-ssl + jar + 1.5.0 + guacamole-auth-sso-ssl + http://guacamole.apache.org/ + + + org.apache.guacamole + guacamole-auth-sso + 1.5.0 + ../../ + + + + + + + org.apache.guacamole + guacamole-ext + + + + + org.apache.guacamole + guacamole-auth-sso-base + + + + + com.google.inject + guice + + + + + javax.servlet + servlet-api + + + + + javax.ws.rs + jsr311-api + + + + + diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java new file mode 100644 index 000000000..16414688e --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java @@ -0,0 +1,413 @@ +/* + * 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.ssl; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; +import org.apache.guacamole.form.Field; +import org.apache.guacamole.form.RedirectField; +import org.apache.guacamole.language.TranslatableMessage; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.credentials.CredentialsInfo; +import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; +import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; + +/** + * Service that authenticates Guacamole users using SSL/TLS authentication + * provided by an external SSL termination service. + */ +@Singleton +public class AuthenticationProviderService implements SSOAuthenticationProviderService { + + /** + * Service for retrieving configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Service for validating and generating unique nonce values. Here, these + * nonces are used specifically for generating unique domains. + */ + @Inject + private NonceService subdomainNonceService; + + /** + * Session manager for generating and maintaining unique tokens to + * represent the authentication flow of a user who has only partially + * authenticated. Here, these tokens represent a user that has been + * validated by SSL termination and allow the Guacamole instance that + * doesn't require SSL/TLS authentication to retrieve the user's identity + * and complete the authentication process. + */ + @Inject + private SSLAuthenticationSessionManager sessionManager; + + /** + * Provider for AuthenticatedUser objects. + */ + @Inject + private Provider authenticatedUserProvider; + + /** + * The string value that the SSL termination service uses for its client + * verification header to represent that the client certificate has been + * verified. + */ + private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; + + /** + * The name of the query parameter containing the temporary session token + * representing the current state of an in-progress authentication attempt. + */ + private static final String AUTH_SESSION_PARAMETER_NAME = "state"; + + /** + * Decodes the provided URL-encoded string as UTF-8, returning the result. + * + * @param value + * The URL-encoded string to decode. + * + * @return + * The decoded string. + * + * @throws GuacamoleException + * If the provided value is not a value URL-encoded string. + */ + private byte[] decode(String value) throws GuacamoleException { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8.name()) + .getBytes(StandardCharsets.UTF_8); + } + catch (IllegalArgumentException e) { + throw new GuacamoleClientException("Invalid URL-encoded value.", e); + } + catch (UnsupportedEncodingException e) { + // This should never happen, as UTF-8 is a standard charset that + // the JVM is required to support + throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); + } + } + + /** + * Authenticates a user using HTTP headers containing that user's verified + * X.509 certificate. It is assumed that this certificate is being passed + * to Guacamole from an SSL termination service that has already verified + * that this certificate is valid and authorized for access to that + * Guacamole instance. + * + * @param credentials + * The credentials received by Guacamole in the authentication request. + * + * @param certificate + * The raw bytes of the X.509 certificate retrieved from the request. + * + * @return + * A new SSOAuthenticatedUser representing the identity of the user + * asserted by the SSL termination service via that user's X.509 + * certificate. + * + * @throws GuacamoleException + * If the provided X.509 certificate is not valid or cannot be parsed. + * It is expected that the SSL termination service will already have + * validated the certificate; this function validates only the + * certificate timestamps. + */ + private SSOAuthenticatedUser authenticateUser(Credentials credentials, + byte[] certificate) throws GuacamoleException { + + // Parse and re-verify certificate is valid with respect to timestamps + X509Certificate cert; + try (InputStream input = new ByteArrayInputStream(certificate)) { + + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + cert = (X509Certificate) certFactory.generateCertificate(input); + + // Verify certificate is valid (it should be given pre-validation + // from SSL termination, but it's worth rechecking for sanity) + cert.checkValidity(); + + } + catch (CertificateException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented is not valid.", e); + } + catch (IOException e) { + throw new GuacamoleServerException("Provided X.509 certificate " + + "could not be read.", e); + } + + // Extract user's DN from their X.509 certificate + LdapName dn; + try { + Principal principal = cert.getSubjectX500Principal(); + dn = new LdapName(principal.getName()); + } + catch (InvalidNameException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented does not contain a valid subject DN.", e); + } + + // Verify DN actually contains components + int numComponents = dn.size(); + if (numComponents < 1) + throw new GuacamoleClientException("The X.509 certificate " + + "presented contains an empty subject DN."); + + // Simply use first component of DN as username (TODO: Enforce + // requirements on the attribute providing the username and the base DN, + // and consider using components following the username to determine + // group memberships) + String username = dn.getRdn(numComponents - 1).getValue().toString(); + + SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); + authenticatedUser.init(username, credentials, + Collections.emptySet(), Collections.emptyMap()); + return authenticatedUser; + + } + + /** + * Processes the given HTTP request, returning the identity represented by + * the auth session token present in that request. If no such token is + * present, or the token does not represent a valid identity, null is + * returned. + * + * @param request + * The HTTP request to process. + * + * @return + * The identity represented by the auth session token in the request, + * or null if there is no such token or the token does not represent a + * valid identity. + */ + private SSOAuthenticatedUser processIdentity(HttpServletRequest request) { + String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME); + return sessionManager.getIdentity(state); + } + + /** + * Processes the X.509 certificate in the headers of the given HTTP + * request, returning an authentication session token representing the + * identity in that certificate. If the certificate is invalid or not + * present, null is returned. + * + * @param credentials + * The credentials submitted in the HTTP request being processed. + * + * @param request + * The HTTP request to process. + * + * @return + * An authentication session token representing the identity in the + * certificate in the given HTTP request, or null if the request does + * not contain a valid certificate. + * + * @throws GuacamoleException + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed. + */ + private String processCertificate(Credentials credentials, + HttpServletRequest request) throws GuacamoleException { + + // Verify that SSL termination has already verified the certificate + String verified = request.getHeader(confService.getClientVerifiedHeader()); + if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) + return null; + + String certificate = request.getHeader(confService.getClientCertificateHeader()); + if (certificate == null) + return null; + + SSOAuthenticatedUser authenticatedUser = authenticateUser(credentials, decode(certificate)); + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); + return sessionManager.defer(new SSLAuthenticationSession(authenticatedUser, validityDuration)); + + } + + /** + * Redirects the current user back to the main URL of the Guacamole + * instance to continue the authentication process after having identified + * themselves using SSL/TLS client authentication. + * + * @param token + * The authentication session token generated for the current user's + * identity. + * + * @throws GuacamoleException + * To redirect the user to the main URL of the Guacamole instance. + */ + private void resumeAuthenticationAtRedirectURI(String token) + throws GuacamoleException { + + URI redirectURI = UriBuilder.fromUri(confService.getRedirectURI()) + .queryParam(AUTH_SESSION_PARAMETER_NAME, token) + .build(); + + // Request that the provided credentials, now tokenized, be + // resubmitted in that tokenized form to the original host for + // authentication + throw new GuacamoleInsufficientCredentialsException("Please " + + "resubmit your tokenized credentials using the " + + "following URI.", + new CredentialsInfo(Arrays.asList(new Field[] { + new RedirectField(AUTH_SESSION_PARAMETER_NAME, redirectURI, + new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) + })) + ); + + } + + @Override + public SSOAuthenticatedUser authenticateUser(Credentials credentials) + throws GuacamoleException { + + // + // Overall flow: + // + // 1) Unauthenticated user is given a temporary auth session token + // and redirected to the SSL termination instance that provides + // SSL client auth. This redirect uses a unique and temporary + // subdomain to ensure each SSL client auth attempt is fresh and + // does not use cached auth details. + // + // 2) Unauthenticated user with a temporary auth session token + // is validated by SSL termination, with that SSL termination + // adding HTTP headers containing the validated certificate to the + // user's HTTP request. + // + // 3) If valid, the user is assigned a temporary token and redirected + // back to the original URL. That temporary token is accepted by + // this extension at the original URL as proof of the user's + // identity. + // + // NOTE: All SSL termination endpoints in front of Guacamole MUST + // be configured to drop these headers from any inbound requests + // or users may be able to assert arbitrary identities, since this + // extension does not validate anything but the certificate timestamps. + // It relies purely on SSL termination to validate that the certificate + // was signed by the expected CA. + // + + // We can't authenticate using SSL/TLS client auth unless there's an + // associated HTTP request + HttpServletRequest request = credentials.getRequest(); + if (request == null) + return null; + + // We MUST have the domain associated with the request to ensure we + // always get fresh SSL sessions when validating client certificates + String host = request.getHeader("Host"); + if (host == null) + return null; + + // + // Handle only auth session tokens at the main redirect URI, using the + // pre-verified information from those tokens to determine user + // identity. + // + + String redirectHost = confService.getRedirectURI().getHost(); + if (host.equals(redirectHost)) { + + SSOAuthenticatedUser user = processIdentity(request); + if (user != null) + return user; + + // Redirect unauthenticated requests to the endpoint requiring + // SSL client auth to request identity verification + throw new GuacamoleInvalidCredentialsException("Invalid login.", + new CredentialsInfo(Arrays.asList(new Field[] { + new RedirectField(AUTH_SESSION_PARAMETER_NAME, getLoginURI(), // <-- Each call to getLoginURI() produces a unique subdomain that is valid only for ONE use (see below) + new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) + })) + ); + + } + + // + // Process certificates only at valid single-use subdomains dedicated + // to client authentication, redirecting back to the main redirect URI + // for final authentication if that processing is successful. + // + // NOTE: This is CRITICAL. If unique subdomains are not generated and + // tied to strictly one authentication attempt, then those subdomains + // could be reused by a user on a shared machine to assume the cached + // credentials of another user that used that machine earlier. The + // browser and/or OS may cache the certificate so that it can be reused + // for future SSL sessions to that same domain. Here, we ensure each + // generated domain is unique and only valid for certificate processing + // ONCE. The domain may still be valid with DNS, but will no longer be + // usable for certificate authentication. + // + + else if (subdomainNonceService.isValid(confService.getClientAuthenticationSubdomain(host))) { + String token = processCertificate(credentials, request); + if (token != null) + resumeAuthenticationAtRedirectURI(token); + } + + // All other requests are not allowed - refuse to authenticate + throw new GuacamoleClientException("Direct authentication against " + + "this endpoint is not valid without first requesting to " + + "authenticate at the primary URL of this Guacamole " + + "instance."); + + } + + @Override + public URI getLoginURI() throws GuacamoleException { + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); + String uniqueSubdomain = subdomainNonceService.generate(validityDuration); + return confService.getClientAuthenticationURI(uniqueSubdomain); + } + + @Override + public void shutdown() { + sessionManager.shutdown(); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java new file mode 100644 index 000000000..6b01858ef --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.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.ssl; + +import org.apache.guacamole.auth.sso.SSOAuthenticationProvider; +import org.apache.guacamole.auth.sso.SSOResource; + +/** + * Guacamole authentication backend which authenticates users using SSL/TLS + * client authentication provided by some external SSL termination system. This + * SSL termination system must be configured to provide access to this same + * instance of Guacamole and must have both a wildcard certificate and wildcard + * DNS. No storage for connections is provided - only authentication. Storage + * must be provided by some other extension. + */ +public class SSLAuthenticationProvider extends SSOAuthenticationProvider { + + /** + * Creates a new SSLAuthenticationProvider that authenticates users against + * an external SSL termination system using SSL/TLS client authentication. + */ + public SSLAuthenticationProvider() { + super(AuthenticationProviderService.class, SSOResource.class, + new SSLAuthenticationProviderModule()); + } + + @Override + public String getIdentifier() { + return "ssl"; + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java new file mode 100644 index 000000000..56e50bd75 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java @@ -0,0 +1,39 @@ +/* + * 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.ssl; + +import com.google.inject.AbstractModule; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.auth.sso.NonceService; + +/** + * Guice module which configures injections specific to SSO using SSL/TLS + * client authentication. + */ +public class SSLAuthenticationProviderModule extends AbstractModule { + + @Override + protected void configure() { + bind(ConfigurationService.class); + bind(NonceService.class); + bind(SSLAuthenticationSessionManager.class); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java new file mode 100644 index 000000000..e2e0d4c43 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java @@ -0,0 +1,65 @@ +/* + * 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.ssl; + +import org.apache.guacamole.auth.sso.AuthenticationSession; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; + +/** + * Representation of an in-progress SSL/TLS authentication attempt. + */ +public class SSLAuthenticationSession extends AuthenticationSession { + + /** + * The identity asserted by the external SSL termination service. + */ + private final SSOAuthenticatedUser identity; + + /** + * Creates a new AuthenticationSession representing an in-progress SSL/TLS + * authentication attempt. + * + * @param identity + * The identity asserted by the external SSL termination service. This + * MAY NOT be null. + * + * @param expires + * The number of milliseconds that may elapse before this session must + * be considered invalid. + */ + public SSLAuthenticationSession(SSOAuthenticatedUser identity, long expires) { + super(expires); + this.identity = identity; + } + + /** + * Returns the identity asserted by the external SSL termination service. + * As authentication will have completed with respect to the SSL + * termination service by the time this session is created, this will + * always be non-null. + * + * @return + * The identity asserted by the external SSL termination service. + */ + public SSOAuthenticatedUser getIdentity() { + return identity; + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java new file mode 100644 index 000000000..3200cae3d --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java @@ -0,0 +1,61 @@ +/* + * 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.ssl; + +import com.google.inject.Singleton; +import org.apache.guacamole.auth.sso.AuthenticationSessionManager; +import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; + +/** + * Manager service that temporarily stores SSL/TLS authentication attempts + * while the authentication flow is underway. + */ +@Singleton +public class SSLAuthenticationSessionManager + extends AuthenticationSessionManager { + + /** + * Returns the identity asserted by the external SSL termination service at + * the end of the authentication process represented by the authentication + * session with the given identifier. If there is no such authentication + * session, or no valid identity has been asserted for that session, null + * is returned. + * + * @param identifier + * The unique string returned by the call to defer(). For convenience, + * this value may safely be null. + * + * @return + * The identity asserted by the external SSL termination service at the + * end of the authentication process represented by the authentication + * session with the given identifier, or null if there is no such + * identity. + */ + public SSOAuthenticatedUser getIdentity(String identifier) { + + SSLAuthenticationSession session = resume(identifier); + if (session != null) + return session.getIdentity(); + + return null; + + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java new file mode 100644 index 000000000..074ee093b --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java @@ -0,0 +1,325 @@ +/* + * 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.ssl.conf; + +import com.google.inject.Inject; +import java.net.URI; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.environment.Environment; +import org.apache.guacamole.properties.IntegerGuacamoleProperty; +import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.apache.guacamole.properties.URIGuacamoleProperty; + +/** + * Service for retrieving configuration information regarding SSO using SSL/TLS + * authentication. + */ +public class ConfigurationService { + + /** + * The default name of the header to use to retrieve the URL-encoded client + * certificate from an HTTP request received from an SSL termination + * service providing SSL/TLS client authentication. + */ + private static String DEFAULT_CLIENT_CERTIFICATE_HEADER = "X-Client-Certificate"; + + /** + * The default name of the header to use to retrieve the verification + * status of the certificate an HTTP request received from an SSL + * termination service providing SSL/TLS client authentication. + */ + private static String DEFAULT_CLIENT_VERIFIED_HEADER = "X-Client-Verified"; + + /** + * The default amount of time that a temporary authentication token for + * SSL/TLS authentication may remain valid, in minutes. + */ + private static int DEFAULT_MAX_TOKEN_VALIDITY = 5; + + /** + * The default amount of time that the temporary, unique subdomain + * generated for SSL/TLS authentication may remain valid, in minutes. + */ + private static int DEFAULT_MAX_DOMAIN_VALIDITY = 5; + + /** + * The property representing the URI that should be used to authenticate + * users with SSL/TLS client authentication. This must be a URI that points + * to THIS instance of Guacamole, but behind SSL termination that requires + * SSL/TLS client authentication. + */ + private static final WildcardURIGuacamoleProperty SSL_CLIENT_AUTH_URI = + new WildcardURIGuacamoleProperty() { + + @Override + public String getName() { return "ssl-client-auth-uri"; } + + }; + + /** + * The property representing the URI of this instance without SSL/TLS + * client authentication required. This must be a URI that points + * to THIS instance of Guacamole, but behind SSL termination that DOES NOT + * require or request SSL/TLS client authentication. + */ + private static final URIGuacamoleProperty SSL_REDIRECT_URI = + new URIGuacamoleProperty() { + + @Override + public String getName() { return "ssl-redirect-uri"; } + + }; + + /** + * The property representing the name of the header to use to retrieve the + * URL-encoded client certificate from an HTTP request received from an + * SSL termination service providing SSL/TLS client authentication. + */ + private static final StringGuacamoleProperty SSL_CLIENT_CERTIFICATE_HEADER = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "ssl-client-certificate-header"; } + + }; + + /** + * The property representing the name of the header to use to retrieve the + * verification status of the certificate an HTTP request received from an + * SSL termination service providing SSL/TLS client authentication. This + * value of this header must be "SUCCESS" (all uppercase) if the + * certificate was successfully verified. + */ + private static final StringGuacamoleProperty SSL_CLIENT_VERIFIED_HEADER = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "ssl-client-verified-header"; } + + }; + + /** + * The property representing the amount of time that a temporary + * authentication token for SSL/TLS authentication may remain valid, in + * minutes. This token is used to represent the user's asserted identity + * after it has been verified by the SSL termination service. This interval + * must be long enough to allow for network delays in redirecting the user + * back to the main Guacamole URL, but short enough that unused tokens do + * not consume unnecessary server resources and cannot potentially be + * guessed while the token is still valid. These tokens are 256-bit secure + * random values. + */ + private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "ssl-max-token-validity"; } + + }; + + /** + * The property representing the amount of time that the temporary, unique + * subdomain generated for SSL/TLS authentication may remain valid, in + * minutes. This subdomain is used to ensure each SSL/TLS authentication + * attempt is fresh and does not potentially reuse a previous + * authentication attempt that was cached by the browser or OS. This + * interval must be long enough to allow for network delays in redirecting + * the user to the SSL termination service enforcing SSL/TLS + * authentication, but short enough that an unused domain does not consume + * unnecessary server resources and cannot potentially be guessed while + * that subdomain is still valid. These subdomains are 128-bit secure + * random values. + */ + private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY = + new IntegerGuacamoleProperty() { + + @Override + public String getName() { return "ssl-max-domain-validity"; } + + }; + + /** + * The Guacamole server environment. + */ + @Inject + private Environment environment; + + /** + * Returns a URI that should be used to authenticate users with SSL/TLS + * client authentication. The returned URI will consist of the configured + * client authentication URI with the wildcard portion ("*.") replaced with + * the given subdomain. + * + * @param subdomain + * The subdomain that should replace the wildcard portion of the + * configured client authentication URI. + * + * @return + * A URI that should be used to authenticate users with SSL/TLS + * client authentication. + * + * @throws GuacamoleException + * If the required property for configuring the client authentication + * URI is missing or cannot be parsed. + */ + public URI getClientAuthenticationURI(String subdomain) throws GuacamoleException { + + URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI); + String baseHostname = authURI.getHost(); + + // Add provided subdomain to auth URI + return UriBuilder.fromUri(authURI) + .host(subdomain + "." + baseHostname) + .build(); + + } + + /** + * Given a hostname that was used by a user for SSL/TLS client + * authentication, returns the subdomain at the beginning of that hostname. + * If the hostname does not match the pattern of hosts represented by the + * configured client authentication URI, null is returned. + * + * @param hostname + * The hostname to extract the subdomain from. + * + * @return + * The subdomain at the beginning of the provided hostname, if that + * hostname matches the pattern of hosts represented by the + * configured client authentication URI, or null otherwise. + * + * @throws GuacamoleException + * If the required property for configuring the client authentication + * URI is missing or cannot be parsed. + */ + public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException { + + URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI); + String baseHostname = authURI.getHost(); + + // Verify the first domain component is at least one character in + // length + int firstPeriod = hostname.indexOf('.'); + if (firstPeriod <= 0) + return null; + + // Verify domain matches the configured auth URI except for the leading + // subdomain + if (!hostname.regionMatches(true, firstPeriod + 1, baseHostname, 0, baseHostname.length())) + return null; + + // Extract subdomain + return hostname.substring(0, firstPeriod); + + } + + /** + * Returns the URI of this instance without SSL/TLS client authentication + * required. + * + * @return + * The URI of this instance without SSL/TLS client authentication + * required. + * + * @throws GuacamoleException + * If the required property for configuring the redirect URI is missing + * or cannot be parsed. + */ + public URI getRedirectURI() throws GuacamoleException { + return environment.getRequiredProperty(SSL_REDIRECT_URI); + } + + /** + * Returns the name of the header to use to retrieve the URL-encoded client + * certificate from an HTTP request received from an SSL termination + * service providing SSL/TLS client authentication. + * + * @return + * The name of the header to use to retrieve the URL-encoded client + * certificate from an HTTP request received from an SSL termination + * service providing SSL/TLS client authentication. + * + * @throws GuacamoleException + * If the property for configuring the client certificate header cannot + * be parsed. + */ + public String getClientCertificateHeader() throws GuacamoleException { + return environment.getProperty(SSL_CLIENT_CERTIFICATE_HEADER, DEFAULT_CLIENT_CERTIFICATE_HEADER); + } + + /** + * Returns the name of the header to use to retrieve the verification + * status of the certificate an HTTP request received from an SSL + * termination service providing SSL/TLS client authentication. + * + * @return + * The name of the header to use to retrieve the verification + * status of the certificate an HTTP request received from an SSL + * termination service providing SSL/TLS client authentication. + * + * @throws GuacamoleException + * If the property for configuring the client verification header + * cannot be parsed. + */ + public String getClientVerifiedHeader() throws GuacamoleException { + return environment.getProperty(SSL_CLIENT_VERIFIED_HEADER, DEFAULT_CLIENT_VERIFIED_HEADER); + } + + /** + * Returns the maximum amount of time that the token generated by the + * Guacamole server representing current SSL authentication state should + * remain valid, in minutes. This imposes an upper limit on the amount of + * time any particular authentication request can result in successful + * authentication within Guacamole when SSL/TLS client authentication is + * configured. By default, this will be 5. + * + * @return + * The maximum amount of time that an SSL authentication token + * generated by the Guacamole server should remain valid, in minutes. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getMaxTokenValidity() throws GuacamoleException { + return environment.getProperty(SSL_MAX_TOKEN_VALIDITY, DEFAULT_MAX_TOKEN_VALIDITY); + } + + /** + * Returns the maximum amount of time that a unique client authentication + * subdomain generated by the Guacamole server should remain valid, in + * minutes. This imposes an upper limit on the amount of time any + * particular authentication request can result in successful + * authentication within Guacamole when SSL/TLS client authentication is + * configured. By default, this will be 5. + * + * @return + * The maximum amount of time that a unique client authentication + * subdomain generated by the Guacamole server should remain valid, in + * minutes. + * + * @throws GuacamoleException + * If guacamole.properties cannot be parsed. + */ + public int getMaxDomainValidity() throws GuacamoleException { + return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY); + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java new file mode 100644 index 000000000..d237d8031 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java @@ -0,0 +1,66 @@ +/* + * 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.ssl.conf; + +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.properties.URIGuacamoleProperty; + +/** + * A GuacamoleProperty whose value is a wildcard URI. The behavior of this + * property is identical to URIGuacamoleProperty except that it verifies a + * wildcard hostname prefix ("*.") is present and strips that prefix from the + * parsed URI. + */ +public abstract class WildcardURIGuacamoleProperty extends URIGuacamoleProperty { + + /** + * Regular expression that broadly matches URIs that contain wildcards in + * their hostname. This regular expression is NOT strict and will match + * invalid URIs. It is only strict enough to recognize a wildcard hostname + * prefix. + */ + private static final Pattern WILDCARD_URI_PATTERN = Pattern.compile("([^:]+://(?:[^@]+@)?)\\*\\.(.*)"); + + @Override + public URI parseValue(String value) throws GuacamoleException { + + // Verify wildcard prefix is present + Matcher matcher = WILDCARD_URI_PATTERN.matcher(value); + if (matcher.matches()) { + + // Strip wildcard prefix from URI and verify a valid hostname is + // still present + URI uri = super.parseValue(matcher.group(1) + matcher.group(2)); + if (uri.getHost() != null) + return uri; + + } + + // All other values are not valid wildcard URIs + throw new GuacamoleServerException("Value \"" + value + + "\" is not a valid wildcard URI."); + + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json new file mode 100644 index 000000000..30c8a1c99 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json @@ -0,0 +1,33 @@ +{ + + "guacamoleVersion" : "1.5.0", + + "name" : "SSL Authentication Extension", + "namespace" : "ssl", + + "authProviders" : [ + "org.apache.guacamole.auth.ssl.SSLAuthenticationProvider" + ], + + "css" : [ + "styles/sso-providers.css" + ], + + "html" : [ + "html/sso-providers.html", + "html/sso-provider-ssl.html" + ], + + "translations" : [ + "translations/ca.json", + "translations/de.json", + "translations/en.json", + "translations/fr.json", + "translations/ja.json", + "translations/ko.json", + "translations/pt.json", + "translations/ru.json", + "translations/zh.json" + ] + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html new file mode 100644 index 000000000..b6c510931 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html @@ -0,0 +1,4 @@ + +
  • {{ + 'LOGIN.NAME_IDP_SSL' | translate +}}
  • diff --git a/extensions/guacamole-auth-sso/pom.xml b/extensions/guacamole-auth-sso/pom.xml index 04ffe842f..99632e4c3 100644 --- a/extensions/guacamole-auth-sso/pom.xml +++ b/extensions/guacamole-auth-sso/pom.xml @@ -49,6 +49,7 @@ modules/guacamole-auth-sso-cas modules/guacamole-auth-sso-openid modules/guacamole-auth-sso-saml + modules/guacamole-auth-sso-ssl From 9177cac3cc641d561f4c817b721348355943e812 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Jan 2023 12:40:02 -0800 Subject: [PATCH 118/289] GUACAMOLE-839: Allow authenticationService to affect login screen even if not invoked by login screen. --- .../app/auth/service/authenticationService.js | 71 ++++++++++----- .../src/app/login/directives/login.js | 88 +++++++++---------- 2 files changed, 94 insertions(+), 65 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index 8a867cea5..a2faa90ff 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -18,25 +18,45 @@ */ /** - * A service for authenticating a user against the REST API. + * A service for authenticating a user against the REST API. Invoking the + * authenticate() or login() functions of this service will automatically + * affect the login dialog, if visible. * - * This service broadcasts two events on $rootScope depending on the result of - * authentication operations: 'guacLogin' if authentication was successful and - * a new token was created, and 'guacLogout' if an existing token is being - * destroyed or replaced. Both events will be passed the related token as their - * sole parameter. + * This service broadcasts events on $rootScope depending on the status and + * result of authentication operations: * - * If a login attempt results in an existing token being replaced, 'guacLogout' - * will be broadcast first for the token being replaced, followed by - * 'guacLogin' for the new token. - * - * Failed logins may also result in guacInsufficientCredentials or - * guacInvalidCredentials events, if the provided credentials were rejected for - * being insufficient or invalid respectively. Both events will be provided - * the set of parameters originally given to authenticate() and the error that - * rejected the credentials. The Error object provided will contain set of - * expected credentials returned by the REST endpoint. This set of credentials - * will be in the form of a Field array. + * "guacLoginPending" + * An authentication request was submitted and we are awaiting the result. + * This event receives an object containing the HTTP parameters submitted + * as its sole parameter. + * + * "guacLogin" + * Authentication was successful and a new token was created. This event + * receives the authentication token as its sole parameter. + * + * "guacLogout" + * An existing token is being destroyed. This event receives the + * authentication token as its sole parameter. If the existing token for + * the current session is being replaced without destroying that session, + * this event is not fired. + * + * "guacLoginFailed" + * An authentication request has failed for any reason. This event is + * broadcast before any other events that are specific to the nature of + * the failure, and may be used to detect login failures in lieu of those + * events. This event receives two parameters: the HTTP parameters + * submitted and the Error object received from the REST endpoint. + * + * "guacInsufficientCredentials" + * An authentication request failed because additional credentials are + * needed before the request can be processed. This event receives two + * parameters: the HTTP parameters submitted and the Error object received + * from the REST endpoint. + * + * "guacInvalidCredentials" + * An authentication request failed because the credentials provided are + * invalid. This event receives two parameters: the HTTP parameters + * submitted and the Error object received from the REST endpoint. */ angular.module('auth').factory('authenticationService', ['$injector', function authenticationService($injector) { @@ -141,7 +161,8 @@ angular.module('auth').factory('authenticationService', ['$injector', * and given arbitrary parameters, returning a promise that succeeds only * if the authentication operation was successful. The resulting * authentication data can be retrieved later via getCurrentToken() or - * getCurrentUsername(). + * getCurrentUsername(). Invoking this function will affect the UI, + * including the login screen if visible. * * The provided parameters can be virtually any object, as each property * will be sent as an HTTP parameter in the authentication request. @@ -159,6 +180,9 @@ angular.module('auth').factory('authenticationService', ['$injector', */ service.authenticate = function authenticate(parameters) { + // Notify that a fresh authentication request is being submitted + $rootScope.$broadcast('guacLoginPending', parameters); + // Attempt authentication return requestService({ method: 'POST', @@ -202,6 +226,10 @@ angular.module('auth').factory('authenticationService', ['$injector', // If authentication fails, propogate failure to returned promise ['catch'](requestService.createErrorCallback(function authenticationFailed(error) { + // Notify of generic login failure, for any event consumers that + // wish to handle all types of failures at once + $rootScope.$broadcast('guacLoginFailed', parameters, error); + // Request credentials if provided credentials were invalid if (error.type === Error.Type.INVALID_CREDENTIALS) { $rootScope.$broadcast('guacInvalidCredentials', parameters, error); @@ -321,7 +349,8 @@ angular.module('auth').factory('authenticationService', ['$injector', * with a username and password, ignoring any currently-stored token, * returning a promise that succeeds only if the login operation was * successful. The resulting authentication data can be retrieved later - * via getCurrentToken() or getCurrentUsername(). + * via getCurrentToken() or getCurrentUsername(). Invoking this function + * will affect the UI, including the login screen if visible. * * @param {String} username * The username to log in with. @@ -342,7 +371,9 @@ angular.module('auth').factory('authenticationService', ['$injector', /** * Makes a request to logout a user using the token REST API endpoint, * returning a promise that succeeds only if the logout operation was - * successful. + * successful. Invoking this function will affect the UI, causing the + * visible components of the application to be replaced with a status + * message noting that the user has been logged out. * * @returns {Promise} * A promise which succeeds only if the logout operation was diff --git a/guacamole/src/main/frontend/src/app/login/directives/login.js b/guacamole/src/main/frontend/src/app/login/directives/login.js index b7967d798..80ce00fe4 100644 --- a/guacamole/src/main/frontend/src/app/login/directives/login.js +++ b/guacamole/src/main/frontend/src/app/login/directives/login.js @@ -177,51 +177,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() { * authentication service, redirecting to the main view if successful. */ $scope.login = function login() { - - // Authentication is now in progress - $scope.submitted = true; - - // Start with cleared status - $scope.loginError = null; - - // Attempt login once existing session is destroyed - authenticationService.authenticate($scope.enteredValues) - - // Retry route upon success (entered values will be cleared only - // after route change has succeeded as this can take time) - .then(function loginSuccessful() { - $route.reload(); - }) - - // Reset upon failure - ['catch'](requestService.createErrorCallback(function loginFailed(error) { - - // Initial submission is complete and has failed - $scope.submitted = false; - - // Clear out passwords if the credentials were rejected for any reason - if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) { - - // Flag generic error for invalid login - if (error.type === Error.Type.INVALID_CREDENTIALS) - $scope.loginError = { - 'key' : 'LOGIN.ERROR_INVALID_LOGIN' - }; - - // Display error if anything else goes wrong - else - $scope.loginError = error.translatableMessage; - - // Reset all remaining fields to default values, but - // preserve any usernames - angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { - if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues) - $scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE; - }); - } - - })); - + authenticationService.authenticate($scope.enteredValues)['catch'](requestService.IGNORE); }; /** @@ -244,6 +200,48 @@ angular.module('login').directive('guacLogin', [function guacLogin() { }; + // Update UI to reflect in-progress auth status (clear any previous + // errors, flag as pending) + $rootScope.$on('guacLoginPending', function loginSuccessful() { + $scope.submitted = true; + $scope.loginError = null; + }); + + // Retry route upon success (entered values will be cleared only + // after route change has succeeded as this can take time) + $rootScope.$on('guacLogin', function loginSuccessful() { + $route.reload(); + }); + + // Reset upon failure + $rootScope.$on('guacLoginFailed', function loginFailed(event, parameters, error) { + + // Initial submission is complete and has failed + $scope.submitted = false; + + // Clear out passwords if the credentials were rejected for any reason + if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) { + + // Flag generic error for invalid login + if (error.type === Error.Type.INVALID_CREDENTIALS) + $scope.loginError = { + 'key' : 'LOGIN.ERROR_INVALID_LOGIN' + }; + + // Display error if anything else goes wrong + else + $scope.loginError = error.translatableMessage; + + // Reset all remaining fields to default values, but + // preserve any usernames + angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { + if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues) + $scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE; + }); + } + + }); + // Reset state after authentication and routing have succeeded $rootScope.$on('$routeChangeSuccess', function routeChanged() { $scope.enteredValues = {}; From b6ce4776258ba5366a852b6dc950fb55306c28b1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Jan 2023 13:49:18 -0800 Subject: [PATCH 119/289] GUACAMOLE-839: Support deferred retrieval of authentication parameters. --- .../app/auth/service/authenticationService.js | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index a2faa90ff..88143a740 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -26,9 +26,11 @@ * result of authentication operations: * * "guacLoginPending" - * An authentication request was submitted and we are awaiting the result. - * This event receives an object containing the HTTP parameters submitted - * as its sole parameter. + * An authentication request is being submitted and we are awaiting the + * result. The request may not yet have been submitted if the parameters + * for that request are not ready. This event receives a promise that + * resolves with the HTTP parameters that were ultimately submitted as its + * sole parameter. * * "guacLogin" * Authentication was successful and a new token was created. This event @@ -66,6 +68,7 @@ angular.module('auth').factory('authenticationService', ['$injector', var Error = $injector.get('Error'); // Required services + var $q = $injector.get('$q'); var $rootScope = $injector.get('$rootScope'); var localStorageService = $injector.get('localStorageService'); var requestService = $injector.get('requestService'); @@ -172,58 +175,69 @@ angular.module('auth').factory('authenticationService', ['$injector', * * If a token is provided, it will be reused if possible. * - * @param {Object} parameters - * Arbitrary parameters to authenticate with. + * @param {Object|Promise} parameters + * Arbitrary parameters to authenticate with. If a Promise is provided, + * that Promise must resolve with the parameters to be submitted when + * those parameters are available, and any error will be handled as if + * from the authentication endpoint of the REST API itself. * * @returns {Promise} * A promise which succeeds only if the login operation was successful. */ service.authenticate = function authenticate(parameters) { - // Notify that a fresh authentication request is being submitted + // Coerce received parameters object into a Promise, if it isn't + // already a Promise + parameters = $q.resolve(parameters); + + // Notify that a fresh authentication request is underway $rootScope.$broadcast('guacLoginPending', parameters); - // Attempt authentication - return requestService({ - method: 'POST', - url: 'api/tokens', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: $.param(parameters) - }) + // Attempt authentication after auth parameters are available ... + return parameters.then(function requestParametersReady(requestParams) { - // If authentication succeeds, handle received auth data - .then(function authenticationSuccessful(data) { + return requestService({ + method: 'POST', + url: 'api/tokens', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: $.param(requestParams) + }) - var currentToken = service.getCurrentToken(); + // ... if authentication succeeds, handle received auth data ... + .then(function authenticationSuccessful(data) { - // If a new token was received, ensure the old token is invalidated, - // if any, and notify listeners of the new token - if (data.authToken !== currentToken) { + var currentToken = service.getCurrentToken(); + + // If a new token was received, ensure the old token is invalidated, + // if any, and notify listeners of the new token + if (data.authToken !== currentToken) { + + // If an old token existed, request that the token be revoked + if (currentToken) { + service.revokeToken(currentToken).catch(angular.noop); + } + + // Notify of login and new token + setAuthenticationResult(new AuthenticationResult(data)); + $rootScope.$broadcast('guacLogin', data.authToken); - // If an old token existed, request that the token be revoked - if (currentToken) { - service.revokeToken(currentToken).catch(angular.noop); } - // Notify of login and new token - setAuthenticationResult(new AuthenticationResult(data)); - $rootScope.$broadcast('guacLogin', data.authToken); + // Update cached authentication result, even if the token remains + // the same + else + setAuthenticationResult(new AuthenticationResult(data)); - } + // Authentication was successful + return data; - // Update cached authentication result, even if the token remains - // the same - else - setAuthenticationResult(new AuthenticationResult(data)); - - // Authentication was successful - return data; + }); }) - // If authentication fails, propogate failure to returned promise + // ... if authentication fails, propogate failure to returned promise ['catch'](requestService.createErrorCallback(function authenticationFailed(error) { // Notify of generic login failure, for any event consumers that From 38f1360dec7e00ba4fededfa40001d0a4b02e86a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 27 Jan 2023 16:38:45 -0800 Subject: [PATCH 120/289] GUACAMOLE-839: Ensure SSL/TLS client auth failures are reflected in the Guacamole UI. --- .../sso/AuthenticationSessionManager.java | 14 + .../modules/guacamole-auth-sso-ssl/pom.xml | 50 +++ .../ssl/AuthenticationProviderService.java | 296 ++-------------- .../auth/ssl/OpaqueAuthenticationResult.java | 65 ++++ .../auth/ssl/SSLAuthenticationProvider.java | 3 +- .../ssl/SSLAuthenticationProviderModule.java | 3 +- .../auth/ssl/SSLAuthenticationSession.java | 7 +- .../ssl/SSLAuthenticationSessionManager.java | 3 +- .../ssl/SSLClientAuthenticationResource.java | 332 ++++++++++++++++++ .../auth/ssl/conf/ConfigurationService.java | 84 ++++- .../main/resources/directives/guacSslAuth.js | 68 ++++ .../src/main/resources/guac-manifest.json | 2 + .../main/resources/html/sso-provider-ssl.html | 2 +- .../src/main/resources/license.txt | 18 + .../src/main/resources/sslModule.js | 29 ++ 15 files changed, 681 insertions(+), 295 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java index da530f61b..3261228b8 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/java/org/apache/guacamole/auth/sso/AuthenticationSessionManager.java @@ -68,6 +68,20 @@ public class AuthenticationSessionManager { }, 1, 1, TimeUnit.MINUTES); } + /** + * Generates a cryptographically-secure value identical in form to the + * session tokens generated by {@link #defer(org.apache.guacamole.auth.sso.AuthenticationSession)} + * but invalid. The returned value is indistinguishable from a valid token, + * but is not a valid token. + * + * @return + * An invalid token value that is indistinguishable from a valid + * token. + */ + public String generateInvalid() { + return idGenerator.generateIdentifier(); + } + /** * Resumes the Guacamole side of the authentication process that was * previously deferred through a call to defer(). Once invoked, the diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml index a107e6b7f..c6542ae56 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml @@ -37,6 +37,56 @@ ../../ + + + + + + com.github.buckelieg + minify-maven-plugin + + + default-cli + + UTF-8 + + ${basedir}/src/main/resources + ${project.build.directory}/classes + + / + / + ssl.js + + + license.txt + + + + **/*.js + + + + + **/*.test.js + + CLOSURE + + + + OFF + OFF + + + + + minify + + + + + + + diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java index 16414688e..bc311de47 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java @@ -22,38 +22,16 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.Principal; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.Collections; -import java.util.concurrent.TimeUnit; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleServerException; -import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; -import org.apache.guacamole.form.Field; -import org.apache.guacamole.form.RedirectField; -import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.Credentials; -import org.apache.guacamole.net.auth.credentials.CredentialsInfo; -import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; -import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; /** * Service that authenticates Guacamole users using SSL/TLS authentication @@ -68,13 +46,6 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Inject private ConfigurationService confService; - /** - * Service for validating and generating unique nonce values. Here, these - * nonces are used specifically for generating unique domains. - */ - @Inject - private NonceService subdomainNonceService; - /** * Session manager for generating and maintaining unique tokens to * represent the authentication flow of a user who has only partially @@ -92,124 +63,12 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Inject private Provider authenticatedUserProvider; - /** - * The string value that the SSL termination service uses for its client - * verification header to represent that the client certificate has been - * verified. - */ - private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; - /** * The name of the query parameter containing the temporary session token * representing the current state of an in-progress authentication attempt. */ private static final String AUTH_SESSION_PARAMETER_NAME = "state"; - /** - * Decodes the provided URL-encoded string as UTF-8, returning the result. - * - * @param value - * The URL-encoded string to decode. - * - * @return - * The decoded string. - * - * @throws GuacamoleException - * If the provided value is not a value URL-encoded string. - */ - private byte[] decode(String value) throws GuacamoleException { - try { - return URLDecoder.decode(value, StandardCharsets.UTF_8.name()) - .getBytes(StandardCharsets.UTF_8); - } - catch (IllegalArgumentException e) { - throw new GuacamoleClientException("Invalid URL-encoded value.", e); - } - catch (UnsupportedEncodingException e) { - // This should never happen, as UTF-8 is a standard charset that - // the JVM is required to support - throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); - } - } - - /** - * Authenticates a user using HTTP headers containing that user's verified - * X.509 certificate. It is assumed that this certificate is being passed - * to Guacamole from an SSL termination service that has already verified - * that this certificate is valid and authorized for access to that - * Guacamole instance. - * - * @param credentials - * The credentials received by Guacamole in the authentication request. - * - * @param certificate - * The raw bytes of the X.509 certificate retrieved from the request. - * - * @return - * A new SSOAuthenticatedUser representing the identity of the user - * asserted by the SSL termination service via that user's X.509 - * certificate. - * - * @throws GuacamoleException - * If the provided X.509 certificate is not valid or cannot be parsed. - * It is expected that the SSL termination service will already have - * validated the certificate; this function validates only the - * certificate timestamps. - */ - private SSOAuthenticatedUser authenticateUser(Credentials credentials, - byte[] certificate) throws GuacamoleException { - - // Parse and re-verify certificate is valid with respect to timestamps - X509Certificate cert; - try (InputStream input = new ByteArrayInputStream(certificate)) { - - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - cert = (X509Certificate) certFactory.generateCertificate(input); - - // Verify certificate is valid (it should be given pre-validation - // from SSL termination, but it's worth rechecking for sanity) - cert.checkValidity(); - - } - catch (CertificateException e) { - throw new GuacamoleClientException("The X.509 certificate " - + "presented is not valid.", e); - } - catch (IOException e) { - throw new GuacamoleServerException("Provided X.509 certificate " - + "could not be read.", e); - } - - // Extract user's DN from their X.509 certificate - LdapName dn; - try { - Principal principal = cert.getSubjectX500Principal(); - dn = new LdapName(principal.getName()); - } - catch (InvalidNameException e) { - throw new GuacamoleClientException("The X.509 certificate " - + "presented does not contain a valid subject DN.", e); - } - - // Verify DN actually contains components - int numComponents = dn.size(); - if (numComponents < 1) - throw new GuacamoleClientException("The X.509 certificate " - + "presented contains an empty subject DN."); - - // Simply use first component of DN as username (TODO: Enforce - // requirements on the attribute providing the username and the base DN, - // and consider using components following the username to determine - // group memberships) - String username = dn.getRdn(numComponents - 1).getValue().toString(); - - SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); - authenticatedUser.init(username, credentials, - Collections.emptySet(), Collections.emptyMap()); - return authenticatedUser; - - } - /** * Processes the given HTTP request, returning the identity represented by * the auth session token present in that request. If no such token is @@ -224,80 +83,17 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS * or null if there is no such token or the token does not represent a * valid identity. */ - private SSOAuthenticatedUser processIdentity(HttpServletRequest request) { + private SSOAuthenticatedUser processIdentity(Credentials credentials, HttpServletRequest request) { + String state = request.getParameter(AUTH_SESSION_PARAMETER_NAME); - return sessionManager.getIdentity(state); - } - - /** - * Processes the X.509 certificate in the headers of the given HTTP - * request, returning an authentication session token representing the - * identity in that certificate. If the certificate is invalid or not - * present, null is returned. - * - * @param credentials - * The credentials submitted in the HTTP request being processed. - * - * @param request - * The HTTP request to process. - * - * @return - * An authentication session token representing the identity in the - * certificate in the given HTTP request, or null if the request does - * not contain a valid certificate. - * - * @throws GuacamoleException - * If any configuration parameters related to retrieving certificates - * from HTTP request cannot be parsed. - */ - private String processCertificate(Credentials credentials, - HttpServletRequest request) throws GuacamoleException { - - // Verify that SSL termination has already verified the certificate - String verified = request.getHeader(confService.getClientVerifiedHeader()); - if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) + String username = sessionManager.getIdentity(state); + if (username == null) return null; - String certificate = request.getHeader(confService.getClientCertificateHeader()); - if (certificate == null) - return null; - - SSOAuthenticatedUser authenticatedUser = authenticateUser(credentials, decode(certificate)); - long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); - return sessionManager.defer(new SSLAuthenticationSession(authenticatedUser, validityDuration)); - - } - - /** - * Redirects the current user back to the main URL of the Guacamole - * instance to continue the authentication process after having identified - * themselves using SSL/TLS client authentication. - * - * @param token - * The authentication session token generated for the current user's - * identity. - * - * @throws GuacamoleException - * To redirect the user to the main URL of the Guacamole instance. - */ - private void resumeAuthenticationAtRedirectURI(String token) - throws GuacamoleException { - - URI redirectURI = UriBuilder.fromUri(confService.getRedirectURI()) - .queryParam(AUTH_SESSION_PARAMETER_NAME, token) - .build(); - - // Request that the provided credentials, now tokenized, be - // resubmitted in that tokenized form to the original host for - // authentication - throw new GuacamoleInsufficientCredentialsException("Please " - + "resubmit your tokenized credentials using the " - + "following URI.", - new CredentialsInfo(Arrays.asList(new Field[] { - new RedirectField(AUTH_SESSION_PARAMETER_NAME, redirectURI, - new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) - })) - ); + SSOAuthenticatedUser authenticatedUser = authenticatedUserProvider.get(); + authenticatedUser.init(username, credentials, + Collections.emptySet(), Collections.emptyMap()); + return authenticatedUser; } @@ -308,21 +104,22 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS // // Overall flow: // - // 1) Unauthenticated user is given a temporary auth session token - // and redirected to the SSL termination instance that provides - // SSL client auth. This redirect uses a unique and temporary - // subdomain to ensure each SSL client auth attempt is fresh and - // does not use cached auth details. + // 1) An unauthenticated user makes a GET request to + // ".../api/ext/ssl/identity". After a series of redirects + // intended to prevent that identity from being inadvertently + // cached and inherited by future authentication attempts on the + // same client machine, an external SSL termination service requests + // and validates the user's certificate, those details are passed + // back to Guacamole via HTTP headers, and Guacamole produces a JSON + // response containing an opaque state value. // - // 2) Unauthenticated user with a temporary auth session token - // is validated by SSL termination, with that SSL termination - // adding HTTP headers containing the validated certificate to the - // user's HTTP request. + // 2) The user (still unauthenticated) resubmits the opaque state + // value from the received JSON as the "state" parameter of a + // standard Guacamole authentication request (".../api/tokens"). // - // 3) If valid, the user is assigned a temporary token and redirected - // back to the original URL. That temporary token is accepted by - // this extension at the original URL as proof of the user's - // identity. + // 3) If the certificate received was valid, the user is authenticated + // according to the identity asserted by that certificate. If not, + // authentication is refused. // // NOTE: All SSL termination endpoints in front of Guacamole MUST // be configured to drop these headers from any inbound requests @@ -345,50 +142,13 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS return null; // - // Handle only auth session tokens at the main redirect URI, using the + // Handle only auth session tokens at the primary URI, using the // pre-verified information from those tokens to determine user // identity. // - String redirectHost = confService.getRedirectURI().getHost(); - if (host.equals(redirectHost)) { - - SSOAuthenticatedUser user = processIdentity(request); - if (user != null) - return user; - - // Redirect unauthenticated requests to the endpoint requiring - // SSL client auth to request identity verification - throw new GuacamoleInvalidCredentialsException("Invalid login.", - new CredentialsInfo(Arrays.asList(new Field[] { - new RedirectField(AUTH_SESSION_PARAMETER_NAME, getLoginURI(), // <-- Each call to getLoginURI() produces a unique subdomain that is valid only for ONE use (see below) - new TranslatableMessage("LOGIN.INFO_IDP_REDIRECT_PENDING")) - })) - ); - - } - - // - // Process certificates only at valid single-use subdomains dedicated - // to client authentication, redirecting back to the main redirect URI - // for final authentication if that processing is successful. - // - // NOTE: This is CRITICAL. If unique subdomains are not generated and - // tied to strictly one authentication attempt, then those subdomains - // could be reused by a user on a shared machine to assume the cached - // credentials of another user that used that machine earlier. The - // browser and/or OS may cache the certificate so that it can be reused - // for future SSL sessions to that same domain. Here, we ensure each - // generated domain is unique and only valid for certificate processing - // ONCE. The domain may still be valid with DNS, but will no longer be - // usable for certificate authentication. - // - - else if (subdomainNonceService.isValid(confService.getClientAuthenticationSubdomain(host))) { - String token = processCertificate(credentials, request); - if (token != null) - resumeAuthenticationAtRedirectURI(token); - } + if (confService.isPrimaryHostname(host)) + return processIdentity(credentials, request); // All other requests are not allowed - refuse to authenticate throw new GuacamoleClientException("Direct authentication against " @@ -400,9 +160,7 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS @Override public URI getLoginURI() throws GuacamoleException { - long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); - String uniqueSubdomain = subdomainNonceService.generate(validityDuration); - return confService.getClientAuthenticationURI(uniqueSubdomain); + throw new GuacamoleResourceNotFoundException("No such resource."); } @Override diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java new file mode 100644 index 000000000..66020f20a --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/OpaqueAuthenticationResult.java @@ -0,0 +1,65 @@ +/* + * 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.ssl; + +/** + * REST API response that reports the result of attempting to authenticate the + * user using SSL/TLS client authentication. The information within this + * result is intentionally opaque and must be resubmitted in a separate + * authentication request for authentication to finally succeed or fail. + */ +public class OpaqueAuthenticationResult { + + /** + * An arbitrary value representing the result of authenticating the + * current user. + */ + private final String state; + + /** + * Creates a new OpaqueAuthenticationResult containing the given opaque + * state value. Successful authentication results must be indistinguishable + * from unsuccessful results with respect to this value. Only using this + * value within ANOTHER authentication attempt can determine whether + * authentication is successful. + * + * @param state + * An arbitrary value representing the result of authenticating the + * current user. + */ + public OpaqueAuthenticationResult(String state) { + this.state = state; + } + + /** + * Returns an arbitrary value representing the result of authenticating the + * current user. This value may be resubmitted as the "state" parameter of + * an authentication request beneath the primary URI of the web application + * to finalize the authentication procedure and determine whether the + * operation has succeeded or failed. + * + * @return + * An arbitrary value representing the result of authenticating the + * current user. + */ + public String getState() { + return state; + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java index 6b01858ef..2458c1a44 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProvider.java @@ -20,7 +20,6 @@ package org.apache.guacamole.auth.ssl; import org.apache.guacamole.auth.sso.SSOAuthenticationProvider; -import org.apache.guacamole.auth.sso.SSOResource; /** * Guacamole authentication backend which authenticates users using SSL/TLS @@ -37,7 +36,7 @@ public class SSLAuthenticationProvider extends SSOAuthenticationProvider { * an external SSL termination system using SSL/TLS client authentication. */ public SSLAuthenticationProvider() { - super(AuthenticationProviderService.class, SSOResource.class, + super(AuthenticationProviderService.class, SSLClientAuthenticationResource.class, new SSLAuthenticationProviderModule()); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java index 56e50bd75..46eeaa94f 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationProviderModule.java @@ -20,6 +20,7 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.AbstractModule; +import com.google.inject.Scopes; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.auth.sso.NonceService; @@ -32,7 +33,7 @@ public class SSLAuthenticationProviderModule extends AbstractModule { @Override protected void configure() { bind(ConfigurationService.class); - bind(NonceService.class); + bind(NonceService.class).in(Scopes.SINGLETON); bind(SSLAuthenticationSessionManager.class); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java index e2e0d4c43..4a4c9ce8f 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSession.java @@ -20,7 +20,6 @@ package org.apache.guacamole.auth.ssl; import org.apache.guacamole.auth.sso.AuthenticationSession; -import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; /** * Representation of an in-progress SSL/TLS authentication attempt. @@ -30,7 +29,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { /** * The identity asserted by the external SSL termination service. */ - private final SSOAuthenticatedUser identity; + private final String identity; /** * Creates a new AuthenticationSession representing an in-progress SSL/TLS @@ -44,7 +43,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { * The number of milliseconds that may elapse before this session must * be considered invalid. */ - public SSLAuthenticationSession(SSOAuthenticatedUser identity, long expires) { + public SSLAuthenticationSession(String identity, long expires) { super(expires); this.identity = identity; } @@ -58,7 +57,7 @@ public class SSLAuthenticationSession extends AuthenticationSession { * @return * The identity asserted by the external SSL termination service. */ - public SSOAuthenticatedUser getIdentity() { + public String getIdentity() { return identity; } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java index 3200cae3d..fc1b0842f 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLAuthenticationSessionManager.java @@ -21,7 +21,6 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.Singleton; import org.apache.guacamole.auth.sso.AuthenticationSessionManager; -import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; /** * Manager service that temporarily stores SSL/TLS authentication attempts @@ -48,7 +47,7 @@ public class SSLAuthenticationSessionManager * session with the given identifier, or null if there is no such * identity. */ - public SSOAuthenticatedUser getIdentity(String identifier) { + public String getIdentity(String identifier) { SSLAuthenticationSession session = resume(identifier); if (session != null) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java new file mode 100644 index 000000000..4c99394f5 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -0,0 +1,332 @@ +/* + * 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.ssl; + +import com.google.inject.Inject; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.ws.rs.GET; +import javax.ws.rs.core.Response; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriBuilder; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.auth.ssl.conf.ConfigurationService; +import org.apache.guacamole.auth.sso.NonceService; +import org.apache.guacamole.auth.sso.SSOResource; + +/** + * REST API resource that allows the user to retrieve an opaque state value + * representing their identity as determined by SSL/TLS client authentication. + * The opaque value may represent a valid identity or an authentication + * failure, and must be resubmitted within a normal Guacamole authentication + * request to finalize the authentication process. + */ +public class SSLClientAuthenticationResource extends SSOResource { + + /** + * The string value that the SSL termination service uses for its client + * verification header to represent that the client certificate has been + * verified. + */ + private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; + + /** + * Service for retrieving configuration information. + */ + @Inject + private ConfigurationService confService; + + /** + * Session manager for generating and maintaining unique tokens to + * represent the authentication flow of a user who has only partially + * authenticated. Here, these tokens represent a user that has been + * validated by SSL termination and allow the Guacamole instance that + * doesn't require SSL/TLS authentication to retrieve the user's identity + * and complete the authentication process. + */ + @Inject + private SSLAuthenticationSessionManager sessionManager; + + /** + * Service for validating and generating unique nonce values. Here, these + * nonces are used specifically for generating unique domains. + */ + @Inject + private NonceService subdomainNonceService; + + /** + * Retrieves a single value from the HTTP header having the given name. If + * there are multiple HTTP headers present with this name, the first + * matching header in the request is used. If there are no such headers in + * the request, null is returned. + * + * @param headers + * The HTTP headers present in the request. + * + * @param name + * The name of the header to retrieve. + * + * @return + * The first value of the HTTP header having the given name, or null if + * there is no such header. + */ + private String getHeader(HttpHeaders headers, String name) { + + List values = headers.getRequestHeader(name); + if (values.isEmpty()) + return null; + + return values.get(0); + + } + + /** + * Decodes the provided URL-encoded string as UTF-8, returning the result. + * + * @param value + * The URL-encoded string to decode. + * + * @return + * The decoded string. + * + * @throws GuacamoleException + * If the provided value is not a value URL-encoded string. + */ + private byte[] decode(String value) throws GuacamoleException { + try { + return URLDecoder.decode(value, StandardCharsets.UTF_8.name()) + .getBytes(StandardCharsets.UTF_8); + } + catch (IllegalArgumentException e) { + throw new GuacamoleClientException("Invalid URL-encoded value.", e); + } + catch (UnsupportedEncodingException e) { + // This should never happen, as UTF-8 is a standard charset that + // the JVM is required to support + throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e); + } + } + + /** + * Authenticates a user using HTTP headers containing that user's verified + * X.509 certificate. It is assumed that this certificate is being passed + * to Guacamole from an SSL termination service that has already verified + * that this certificate is valid and authorized for access to that + * Guacamole instance. + * + * @param certificate + * The raw bytes of the X.509 certificate retrieved from the request. + * + * @return + * A new SSOAuthenticatedUser representing the identity of the user + * asserted by the SSL termination service via that user's X.509 + * certificate. + * + * @throws GuacamoleException + * If the provided X.509 certificate is not valid or cannot be parsed. + * It is expected that the SSL termination service will already have + * validated the certificate; this function validates only the + * certificate timestamps. + */ + public String getUsername(byte[] certificate) throws GuacamoleException { + + // Parse and re-verify certificate is valid with respect to timestamps + X509Certificate cert; + try (InputStream input = new ByteArrayInputStream(certificate)) { + + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + cert = (X509Certificate) certFactory.generateCertificate(input); + + // Verify certificate is valid (it should be given pre-validation + // from SSL termination, but it's worth rechecking for sanity) + cert.checkValidity(); + + } + catch (CertificateException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented is not valid.", e); + } + catch (IOException e) { + throw new GuacamoleServerException("Provided X.509 certificate " + + "could not be read.", e); + } + + // Extract user's DN from their X.509 certificate + LdapName dn; + try { + Principal principal = cert.getSubjectX500Principal(); + dn = new LdapName(principal.getName()); + } + catch (InvalidNameException e) { + throw new GuacamoleClientException("The X.509 certificate " + + "presented does not contain a valid subject DN.", e); + } + + // Verify DN actually contains components + int numComponents = dn.size(); + if (numComponents < 1) + throw new GuacamoleClientException("The X.509 certificate " + + "presented contains an empty subject DN."); + + // Simply use first component of DN as username (TODO: Enforce + // requirements on the attribute providing the username and the base DN, + // and consider using components following the username to determine + // group memberships) + return dn.getRdn(numComponents - 1).getValue().toString(); + + } + + /** + * Processes the X.509 certificate in the headers of the given HTTP + * request, returning an authentication session token representing the + * identity in that certificate. If the certificate is invalid or not + * present, null is returned. + * + * @param credentials + * The credentials submitted in the HTTP request being processed. + * + * @param request + * The HTTP request to process. + * + * @return + * An authentication session token representing the identity in the + * certificate in the given HTTP request, or null if the request does + * not contain a valid certificate. + * + * @throws GuacamoleException + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed. + */ + private String processCertificate(HttpHeaders headers) throws GuacamoleException { + + // + // NOTE: A result with an associated state is ALWAYS returned by + // processCertificate(), even if the request does not actually contain + // a valid certificate. This is by design and ensures that the nature + // of a certificate (valid vs. invalid) cannot be determined except + // via Guacamole's authentication endpoint, thus allowing auth failure + // hooks to consider attempts to use invalid certificates as auth + // failures. + // + + // Verify that SSL termination has already verified the certificate + String verified = getHeader(headers, confService.getClientVerifiedHeader()); + if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) + return sessionManager.generateInvalid(); + + String certificate = getHeader(headers, confService.getClientCertificateHeader()); + if (certificate == null) + return sessionManager.generateInvalid(); + + String username = getUsername(decode(certificate)); + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); + return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); + + } + + /** + * Attempts to authenticate the current user using SSL/TLS client + * authentication, returning an opaque value that represents their + * authenticated status. If necessary, the user is first redirected to a + * unique endpoint that supports SSL/TLS client authentication. + * + * @param headers + * All HTTP headers submitted in the user's authentication request. + * + * @param host + * The hostname that the user specified in their HTTP request. + * + * @return + * A Response containing an opaque value representing the user's + * authenticated status, or a Response redirecting the user to a + * unique endpoint that can provide this. + * + * @throws GuacamoleException + * If any required configuration information is missing or cannot be + * parsed, or if the request was not received at a valid subdomain. + */ + @GET + @Path("identity") + public Response authenticateClient(@Context HttpHeaders headers, + @HeaderParam("Host") String host) throws GuacamoleException { + + // Redirect any requests to the domain that does NOT require SSL/TLS + // client authentication to the same endpoint at a domain that does + // require SSL/TLS authentication + String subdomain = confService.getClientAuthenticationSubdomain(host); + if (subdomain == null) { + + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxDomainValidity()); + String uniqueSubdomain = subdomainNonceService.generate(validityDuration); + + URI clientAuthURI = UriBuilder.fromUri(confService.getClientAuthenticationURI(uniqueSubdomain)) + .path("api/ext/ssl/identity") + .build(); + + return Response.seeOther(clientAuthURI).build(); + + } + + // + // Process certificates only at valid single-use subdomains dedicated + // to client authentication, redirecting back to the main redirect URI + // for final authentication if that processing is successful. + // + // NOTE: This is CRITICAL. If unique subdomains are not generated and + // tied to strictly one authentication attempt, then those subdomains + // could be reused by a user on a shared machine to assume the cached + // credentials of another user that used that machine earlier. The + // browser and/or OS may cache the certificate so that it can be reused + // for future SSL sessions to that same domain. Here, we ensure each + // generated domain is unique and only valid for certificate processing + // ONCE. The domain may still be valid with DNS, but will no longer be + // usable for certificate authentication. + // + + if (subdomainNonceService.isValid(subdomain)) + return Response.ok(new OpaqueAuthenticationResult(processCertificate(headers))) + .header("Access-Control-Allow-Origin", confService.getPrimaryOrigin().toString()) + .type(MediaType.APPLICATION_JSON) + .build(); + + throw new GuacamoleResourceNotFoundException("No such resource."); + + } + +} diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java index 074ee093b..3f57bd864 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java @@ -21,8 +21,10 @@ package org.apache.guacamole.auth.ssl.conf; import com.google.inject.Inject; import java.net.URI; +import java.net.URISyntaxException; import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; @@ -80,11 +82,11 @@ public class ConfigurationService { * to THIS instance of Guacamole, but behind SSL termination that DOES NOT * require or request SSL/TLS client authentication. */ - private static final URIGuacamoleProperty SSL_REDIRECT_URI = + private static final URIGuacamoleProperty SSL_PRIMARY_URI = new URIGuacamoleProperty() { @Override - public String getName() { return "ssl-redirect-uri"; } + public String getName() { return "ssl-primary-uri"; } }; @@ -121,11 +123,10 @@ public class ConfigurationService { * authentication token for SSL/TLS authentication may remain valid, in * minutes. This token is used to represent the user's asserted identity * after it has been verified by the SSL termination service. This interval - * must be long enough to allow for network delays in redirecting the user - * back to the main Guacamole URL, but short enough that unused tokens do - * not consume unnecessary server resources and cannot potentially be - * guessed while the token is still valid. These tokens are 256-bit secure - * random values. + * must be long enough to allow for network delays in receiving the token, + * but short enough that unused tokens do not consume unnecessary server + * resources and cannot potentially be guessed while the token is still + * valid. These tokens are 256-bit secure random values. */ private static final IntegerGuacamoleProperty SSL_MAX_TOKEN_VALIDITY = new IntegerGuacamoleProperty() { @@ -141,12 +142,12 @@ public class ConfigurationService { * minutes. This subdomain is used to ensure each SSL/TLS authentication * attempt is fresh and does not potentially reuse a previous * authentication attempt that was cached by the browser or OS. This - * interval must be long enough to allow for network delays in redirecting - * the user to the SSL termination service enforcing SSL/TLS - * authentication, but short enough that an unused domain does not consume - * unnecessary server resources and cannot potentially be guessed while - * that subdomain is still valid. These subdomains are 128-bit secure - * random values. + * interval must be long enough to allow for network delays in + * authenticating the user with the SSL termination service that enforces + * SSL/TLS client authentication, but short enough that an unused domain + * does not consume unnecessary server resources and cannot potentially be + * guessed while that subdomain is still valid. These subdomains are + * 128-bit secure random values. */ private static final IntegerGuacamoleProperty SSL_MAX_DOMAIN_VALIDITY = new IntegerGuacamoleProperty() { @@ -212,6 +213,11 @@ public class ConfigurationService { */ public String getClientAuthenticationSubdomain(String hostname) throws GuacamoleException { + // Any hostname that matches the explicitly-specific primary URI is not + // a client auth subdomain + if (isPrimaryHostname(hostname)) + return null; + URI authURI = environment.getRequiredProperty(SSL_CLIENT_AUTH_URI); String baseHostname = authURI.getHost(); @@ -240,11 +246,57 @@ public class ConfigurationService { * required. * * @throws GuacamoleException - * If the required property for configuring the redirect URI is missing + * If the required property for configuring the primary URI is missing * or cannot be parsed. */ - public URI getRedirectURI() throws GuacamoleException { - return environment.getRequiredProperty(SSL_REDIRECT_URI); + public URI getPrimaryURI() throws GuacamoleException { + return environment.getRequiredProperty(SSL_PRIMARY_URI); + } + + /** + * Returns the HTTP request origin for requests originating from this + * instance via the primary URI (as returned by {@link #getPrimaryURI()}. + * This value is essentially the same as the primary URI but with only the + * scheme, host, and port present. + * + * @return + * The HTTP request origin for requests originating from this instance + * via the primary URI. + * + * @throws GuacamoleException + * If the required property for configuring the primary URI is missing + * or cannot be parsed. + */ + public URI getPrimaryOrigin() throws GuacamoleException { + URI primaryURI = getPrimaryURI(); + try { + return new URI(primaryURI.getScheme(), null, primaryURI.getHost(), primaryURI.getPort(), null, null, null); + } + catch (URISyntaxException e) { + throw new GuacamoleServerException("Request origin could not be " + + "derived from the configured primary URI.", e); + } + } + + /** + * Returns whether the given hostname is the same as the hostname in the + * primary URI (as returned by {@link #getPrimaryURI()}. Hostnames are + * case-insensitive. + * + * @param hostname + * The hostname to test. + * + * @return + * true if the hostname is the same as the hostname in the primary URI, + * false otherwise. + * + * @throws GuacamoleException + * If the required property for configuring the primary URI is missing + * or cannot be parsed. + */ + public boolean isPrimaryHostname(String hostname) throws GuacamoleException { + URI primaryURI = getPrimaryURI(); + return hostname.equalsIgnoreCase(primaryURI.getHost()); } /** diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js new file mode 100644 index 000000000..82e5c8b6d --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js @@ -0,0 +1,68 @@ +/* + * 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. + */ + +/** + * A directive which automatically attempts to log the current user in using + * SSL/TLS client authentication when the associated element is clicked. + */ +angular.module('element').directive('guacSslAuth', ['$injector', function guacSslAuth($injector) { + + // Required services + var requestService = $injector.get('requestService'); + var authenticationService = $injector.get('authenticationService'); + + var directive = { + restrict: 'A' + }; + + directive.link = function linkGuacSslAuth($scope, $element) { + + /** + * The element which will register the click. + * + * @type Element + */ + const element = $element[0]; + + // Attempt SSL/TLS client authentication upon click + element.addEventListener('click', function elementClicked() { + + // Transform SSL/TLS identity into an opaque "state" value and + // attempt authentication using that value + authenticationService.authenticate( + requestService({ + method: 'GET', + headers : { + 'Cache-Control' : undefined, // Avoid sending headers that would result in a pre-flight OPTIONS request for CORS + 'Pragma' : undefined + }, + url: 'api/ext/ssl/identity' + }) + .then(function identityRetrieved(data) { + return { 'state' : data.state || '' }; + }) + )['catch'](requestService.IGNORE); + + }); + + }; + + return directive; + +}]); diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json index 30c8a1c99..7b8099427 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/guac-manifest.json @@ -13,6 +13,8 @@ "styles/sso-providers.css" ], + "js" : [ "ssl.min.js" ], + "html" : [ "html/sso-providers.html", "html/sso-provider-ssl.html" diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html index b6c510931..8798eed2d 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html @@ -1,4 +1,4 @@ -
  • {{ +
  • {{ 'LOGIN.NAME_IDP_SSL' | translate }}
  • diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt new file mode 100644 index 000000000..042f3ce1f --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/license.txt @@ -0,0 +1,18 @@ +/* + * 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. + */ diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js new file mode 100644 index 000000000..2e3f844ef --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/sslModule.js @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * The module for code implementing SSO using SSL/TLS client authentication. + */ +angular.module('guacSsoSsl', [ + 'auth', + 'rest' +]); + +// Ensure the guacSsoSsl module is loaded along with the rest of the app +angular.module('index').requires.push('guacSsoSsl'); From 82553265121ff4df4f52b1eb71f850893b7a22b2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 30 Jan 2023 16:29:04 -0800 Subject: [PATCH 121/289] GUACAMOLE-839: Move SSL/TLS client auth logic to separate service. --- .../main/resources/directives/guacSslAuth.js | 21 +------ .../resources/services/clientAuthService.js | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js index 82e5c8b6d..49347127a 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/directives/guacSslAuth.js @@ -24,8 +24,7 @@ angular.module('element').directive('guacSslAuth', ['$injector', function guacSslAuth($injector) { // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); + var clientAuthService = $injector.get('clientAuthService'); var directive = { restrict: 'A' @@ -42,23 +41,7 @@ angular.module('element').directive('guacSslAuth', ['$injector', function guacSs // Attempt SSL/TLS client authentication upon click element.addEventListener('click', function elementClicked() { - - // Transform SSL/TLS identity into an opaque "state" value and - // attempt authentication using that value - authenticationService.authenticate( - requestService({ - method: 'GET', - headers : { - 'Cache-Control' : undefined, // Avoid sending headers that would result in a pre-flight OPTIONS request for CORS - 'Pragma' : undefined - }, - url: 'api/ext/ssl/identity' - }) - .then(function identityRetrieved(data) { - return { 'state' : data.state || '' }; - }) - )['catch'](requestService.IGNORE); - + clientAuthService.authenticate(); }); }; diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js new file mode 100644 index 000000000..3bf9d9e86 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/services/clientAuthService.js @@ -0,0 +1,58 @@ +/* + * 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. + */ + +/** + * Service for authenticating a user using SSL/TLS client authentication. + */ +angular.module('guacSsoSsl').factory('clientAuthService', ['$injector', + function clientAuthServiceProvider($injector) { + + // Required services + var requestService = $injector.get('requestService'); + var authenticationService = $injector.get('authenticationService'); + + var service = {}; + + /** + * Attempt to authenticate using a unique token obtained through SSL/TLS + * client authentication. + */ + service.authenticate = function authenticate() { + + // Transform SSL/TLS identity into an opaque "state" value and + // attempt authentication using that value + authenticationService.authenticate( + requestService({ + method: 'GET', + headers : { + 'Cache-Control' : undefined, // Avoid sending headers that would result in a pre-flight OPTIONS request for CORS + 'Pragma' : undefined + }, + url: 'api/ext/ssl/identity' + }) + .then(function identityRetrieved(data) { + return { 'state' : data.state || '' }; + }) + )['catch'](requestService.IGNORE); + + }; + + return service; + +}]); From 0b5b82cc486029e3759a33e9224e5ccc3923a039 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 21 Feb 2023 17:06:37 -0800 Subject: [PATCH 122/289] GUACAMOLE-839: Allow accepted subject DNs to be restricted via configuration. --- .../ssl/SSLClientAuthenticationResource.java | 185 +++++++++++++----- .../auth/ssl/conf/ConfigurationService.java | 63 ++++++ .../ssl/conf/LdapNameGuacamoleProperty.java | 50 +++++ 3 files changed, 247 insertions(+), 51 deletions(-) create mode 100644 extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 4c99394f5..21a2e0c25 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import javax.ws.rs.GET; import javax.ws.rs.core.Response; import javax.ws.rs.HeaderParam; @@ -45,10 +46,11 @@ import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; -import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.auth.sso.SSOResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * REST API resource that allows the user to retrieve an opaque state value @@ -66,6 +68,25 @@ public class SSLClientAuthenticationResource extends SSOResource { */ private static final String CLIENT_VERIFIED_HEADER_SUCCESS_VALUE = "SUCCESS"; + /** + * The string value that the SSL termination service uses for its client + * verification header to represent that the client certificate is absent. + */ + private static final String CLIENT_VERIFIED_HEADER_NONE_VALUE = "NONE"; + + /** + * The string prefix that the SSL termination service uses for its client + * verification header to represent that the client certificate has failed + * validation. The error message describing the nature of the failure is + * provided by the SSL termination service after this prefix. + */ + private static final String CLIENT_VERIFIED_HEADER_FAILED_PREFIX = "FAILED:"; + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(SSLClientAuthenticationResource.class); + /** * Service for retrieving configuration information. */ @@ -143,6 +164,67 @@ public class SSLClientAuthenticationResource extends SSOResource { } } + /** + * Extracts a user's username from the X.509 subject name, which should be + * in LDAP DN format. If specific username attributes are configured, only + * those username attributes are used to determine the name. If a specific + * base DN is configured, only subject names that are formatted as LDAP DNs + * within that base DN will be accepted. + * + * @param name + * The subject name to extract the username from. + * + * @return + * The username of the user represented by the given subject name. + * + * @throws GuacamoleException + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed, or if the provided subject name + * cannot be parsed or is not acceptable (wrong base DN or wrong + * username attribute). + */ + public String getUsername(String name) throws GuacamoleException { + + // Extract user's DN from their X.509 certificate + LdapName dn; + try { + dn = new LdapName(name); + } + catch (InvalidNameException e) { + throw new GuacamoleClientException("Subject \"" + name + "\" is " + + "not a valid DN: " + e.getMessage(), e); + } + + // Verify DN actually contains components + int numComponents = dn.size(); + if (numComponents < 1) + throw new GuacamoleClientException("Subject DN is empty."); + + // Verify DN is within configured base DN (if any) + LdapName baseDN = confService.getSubjectBaseDN(); + if (baseDN != null && !(numComponents > baseDN.size() && dn.startsWith(baseDN))) + throw new GuacamoleClientException("Subject DN \"" + dn + "\" is " + + "not within the configured base DN."); + + // Retrieve the least significant attribute from the parsed DN - this + // will be the username + Rdn nameRdn = dn.getRdn(numComponents - 1); + + // Verify that the username is specified with one of the allowed + // attributes + List usernameAttributes = confService.getSubjectUsernameAttributes(); + if (usernameAttributes != null && !usernameAttributes.stream().anyMatch(nameRdn.getType()::equalsIgnoreCase)) + throw new GuacamoleClientException("Subject DN \"" + dn + "\" " + + "does not contain an acceptable username attribute."); + + // The DN is valid - extract the username from the least significant + // component + String username = nameRdn.getValue().toString(); + logger.debug("Username \"{}\" extracted from subject DN \"{}\".", username, dn); + return username; + + } + /** * Authenticates a user using HTTP headers containing that user's verified * X.509 certificate. It is assumed that this certificate is being passed @@ -154,15 +236,13 @@ public class SSLClientAuthenticationResource extends SSOResource { * The raw bytes of the X.509 certificate retrieved from the request. * * @return - * A new SSOAuthenticatedUser representing the identity of the user - * asserted by the SSL termination service via that user's X.509 - * certificate. + * The username of the user asserted by the SSL termination service via + * that user's X.509 certificate. * * @throws GuacamoleException - * If the provided X.509 certificate is not valid or cannot be parsed. - * It is expected that the SSL termination service will already have - * validated the certificate; this function validates only the - * certificate timestamps. + * If any configuration parameters related to retrieving certificates + * from HTTP request cannot be parsed, or if the certificate is not + * valid/present. */ public String getUsername(byte[] certificate) throws GuacamoleException { @@ -179,36 +259,15 @@ public class SSLClientAuthenticationResource extends SSOResource { } catch (CertificateException e) { - throw new GuacamoleClientException("The X.509 certificate " - + "presented is not valid.", e); + throw new GuacamoleClientException("Certificate is not valid: " + e.getMessage(), e); } catch (IOException e) { - throw new GuacamoleServerException("Provided X.509 certificate " - + "could not be read.", e); + throw new GuacamoleClientException("Certificate could not be read: " + e.getMessage(), e); } // Extract user's DN from their X.509 certificate - LdapName dn; - try { - Principal principal = cert.getSubjectX500Principal(); - dn = new LdapName(principal.getName()); - } - catch (InvalidNameException e) { - throw new GuacamoleClientException("The X.509 certificate " - + "presented does not contain a valid subject DN.", e); - } - - // Verify DN actually contains components - int numComponents = dn.size(); - if (numComponents < 1) - throw new GuacamoleClientException("The X.509 certificate " - + "presented contains an empty subject DN."); - - // Simply use first component of DN as username (TODO: Enforce - // requirements on the attribute providing the username and the base DN, - // and consider using components following the username to determine - // group memberships) - return dn.getRdn(numComponents - 1).getValue().toString(); + Principal principal = cert.getSubjectX500Principal(); + return getUsername(principal.getName()); } @@ -216,7 +275,7 @@ public class SSLClientAuthenticationResource extends SSOResource { * Processes the X.509 certificate in the headers of the given HTTP * request, returning an authentication session token representing the * identity in that certificate. If the certificate is invalid or not - * present, null is returned. + * present, an invalid session token is returned. * * @param credentials * The credentials submitted in the HTTP request being processed. @@ -226,14 +285,10 @@ public class SSLClientAuthenticationResource extends SSOResource { * * @return * An authentication session token representing the identity in the - * certificate in the given HTTP request, or null if the request does - * not contain a valid certificate. - * - * @throws GuacamoleException - * If any configuration parameters related to retrieving certificates - * from HTTP request cannot be parsed. + * certificate in the given HTTP request, or an invalid session token + * if no valid identity was asserted. */ - private String processCertificate(HttpHeaders headers) throws GuacamoleException { + private String processCertificate(HttpHeaders headers) { // // NOTE: A result with an associated state is ALWAYS returned by @@ -245,18 +300,46 @@ public class SSLClientAuthenticationResource extends SSOResource { // failures. // - // Verify that SSL termination has already verified the certificate - String verified = getHeader(headers, confService.getClientVerifiedHeader()); - if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) - return sessionManager.generateInvalid(); + try { - String certificate = getHeader(headers, confService.getClientCertificateHeader()); - if (certificate == null) - return sessionManager.generateInvalid(); + // Verify that SSL termination has already verified the certificate + String verified = getHeader(headers, confService.getClientVerifiedHeader()); + if (verified != null && verified.startsWith(CLIENT_VERIFIED_HEADER_FAILED_PREFIX)) { + String message = verified.substring(CLIENT_VERIFIED_HEADER_FAILED_PREFIX.length()); + throw new GuacamoleClientException("Client certificate did " + + "not pass validation. SSL termination reports the " + + "following failure: \"" + message + "\""); + } + else if (CLIENT_VERIFIED_HEADER_NONE_VALUE.equals(verified)) { + throw new GuacamoleClientException("No client certificate was presented."); + } + else if (!CLIENT_VERIFIED_HEADER_SUCCESS_VALUE.equals(verified)) { + throw new GuacamoleClientException("Client certificate did not pass validation."); + } - String username = getUsername(decode(certificate)); - long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); - return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); + String certificate = getHeader(headers, confService.getClientCertificateHeader()); + if (certificate == null) + throw new GuacamoleClientException("Client certificate missing from request."); + + String username = getUsername(decode(certificate)); + long validityDuration = TimeUnit.MINUTES.toMillis(confService.getMaxTokenValidity()); + return sessionManager.defer(new SSLAuthenticationSession(username, validityDuration)); + + } + catch (GuacamoleClientException e) { + logger.warn("SSL/TLS client authentication attempt rejected: {}", e.getMessage()); + logger.debug("SSL/TLS client authentication failed.", e); + } + catch (GuacamoleException e) { + logger.error("SSL/TLS client authentication attempt could not be processed: {}", e.getMessage()); + logger.debug("SSL/TLS client authentication failed.", e); + } + catch (RuntimeException | Error e) { + logger.error("SSL/TLS client authentication attempt failed internally: {}", e.getMessage()); + logger.debug("Internal failure processing SSL/TLS client authentication attempt.", e); + } + + return sessionManager.generateInvalid(); } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java index 3f57bd864..48fc86b65 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/ConfigurationService.java @@ -22,12 +22,15 @@ package org.apache.guacamole.auth.ssl.conf; import com.google.inject.Inject; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; +import javax.naming.ldap.LdapName; import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.properties.IntegerGuacamoleProperty; import org.apache.guacamole.properties.StringGuacamoleProperty; +import org.apache.guacamole.properties.StringListProperty; import org.apache.guacamole.properties.URIGuacamoleProperty; /** @@ -136,6 +139,34 @@ public class ConfigurationService { }; + /** + * The property defining the LDAP attribute or attributes that may be used + * to represent a username within the subject DN of a user's X.509 + * certificate. If the least-significant attribute of the subject DN is not + * one of these attributes, the certificate will be rejected. By default, + * any attribute is accepted. + */ + private static final StringListProperty SSL_SUBJECT_USERNAME_ATTRIBUTE = + new StringListProperty () { + + @Override + public String getName() { return "ssl-subject-username-attribute"; } + + }; + + /** + * The property defining the base DN containing all valid subject DNs. If + * specified, only certificates asserting subject DNs beneath this base DN + * will be accepted. By default, all DNs are accepted. + */ + private static final LdapNameGuacamoleProperty SSL_SUBJECT_BASE_DN = + new LdapNameGuacamoleProperty () { + + @Override + public String getName() { return "ssl-subject-base-dn"; } + + }; + /** * The property representing the amount of time that the temporary, unique * subdomain generated for SSL/TLS authentication may remain valid, in @@ -374,4 +405,36 @@ public class ConfigurationService { return environment.getProperty(SSL_MAX_DOMAIN_VALIDITY, DEFAULT_MAX_DOMAIN_VALIDITY); } + /** + * Returns the base DN that contains all valid subject DNs. If there is no + * such base DN (and all subject DNs are valid), null is returned. + * + * @return + * The base DN that contains all valid subject DNs, or null if all + * subject DNs are valid. + * + * @throws GuacamoleException + * If the configured base DN cannot be read or is not a valid LDAP DN. + */ + public LdapName getSubjectBaseDN() throws GuacamoleException { + return environment.getProperty(SSL_SUBJECT_BASE_DN); + } + + /** + * Returns a list of all attributes that may be used to represent a user's + * username within their subject DN. If all attributes may be accepted, + * null is returned. + * + * @return + * A list of all attributes that may be used to represent a user's + * username within their subject DN, or null if any attribute may be + * used. + * + * @throws GuacamoleException + * If the configured set of username attributes cannot be read. + */ + public List getSubjectUsernameAttributes() throws GuacamoleException { + return environment.getProperty(SSL_SUBJECT_USERNAME_ATTRIBUTE); + } + } diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java new file mode 100644 index 000000000..0299d0163 --- /dev/null +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/LdapNameGuacamoleProperty.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.auth.ssl.conf; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleServerException; +import org.apache.guacamole.properties.GuacamoleProperty; + +/** + * A GuacamoleProperty whose value is an LDAP name, such as a distinguished + * name. + */ +public abstract class LdapNameGuacamoleProperty implements GuacamoleProperty { + + @Override + public LdapName parseValue(String value) throws GuacamoleException { + + if (value == null) + return null; + + try { + return new LdapName(value); + } + catch (InvalidNameException e) { + throw new GuacamoleServerException("Value \"" + value + + "\" is not a valid LDAP name.", e); + } + + } + +} From 632df70162fef96ad35879a664707cc369c81be9 Mon Sep 17 00:00:00 2001 From: Michael Magill Date: Wed, 1 Mar 2023 21:01:30 -0500 Subject: [PATCH 123/289] GUACAMOLE-1738: Update version of MySQL connector GUACAMOLE-1738 Update version of MySQL connector used to resolve issue 1738 where it can no longer connect to the MySQL server using the new extension and old connector: https://issues.apache.org/jira/projects/GUACAMOLE/issues/GUACAMOLE-1738 --- guacamole-docker/bin/build-guacamole.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole-docker/bin/build-guacamole.sh b/guacamole-docker/bin/build-guacamole.sh index 6ffc86611..9c81041c7 100755 --- a/guacamole-docker/bin/build-guacamole.sh +++ b/guacamole-docker/bin/build-guacamole.sh @@ -94,7 +94,7 @@ tar -xzf extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/target/ # echo "Downloading MySQL Connector/J ..." -curl -L "https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.46.tar.gz" | \ +curl -L "https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-j-8.0.32.tar.gz" | \ tar -xz \ -C "$DESTINATION/mysql/" \ --wildcards \ From e0b59bfcd0997db3b5753ae5a5131916d562613b Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Thu, 2 Mar 2023 19:48:16 -0500 Subject: [PATCH 124/289] GUACAMOLE-1730: Fix missing comma guacamole-history-recording-storage guac-manifest.json --- .../src/main/resources/guac-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json index 64d04cdac..e933f9ceb 100644 --- a/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json @@ -10,7 +10,7 @@ ], "translations" : [ - "translations/en.json" + "translations/en.json", "translations/pl.json" ] From b9958fa3318423f4b98cec66f734bd10f31ac2c3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 5 Mar 2023 21:36:54 -0800 Subject: [PATCH 125/289] GUACAMOLE-839: Include SSL/TLS auth support within SSO dist archive. --- .../modules/guacamole-auth-sso-dist/pom.xml | 7 +++++++ .../guacamole-auth-sso-dist/src/main/assembly/dist.xml | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml index f167b8c9b..7bbbd1f70 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/pom.xml @@ -59,6 +59,13 @@ 1.5.0
    + + + org.apache.guacamole + guacamole-auth-sso-ssl + 1.5.0 + + diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml index f122c8d0a..066da2956 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-dist/src/main/assembly/dist.xml @@ -60,6 +60,15 @@ + + + ssl + false + + org.apache.guacamole:guacamole-auth-sso-ssl + + + From f31356c2f54740762f1b141af9013c9495c353b4 Mon Sep 17 00:00:00 2001 From: Alex Leitner Date: Tue, 7 Mar 2023 18:31:45 +0000 Subject: [PATCH 126/289] GUACAMOLE-1740: Prevent collisions of clipboard inactive and active classes with other classes of the same name. --- .../src/main/frontend/src/app/clipboard/styles/clipboard.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/clipboard/styles/clipboard.css b/guacamole/src/main/frontend/src/app/clipboard/styles/clipboard.css index d71287ac7..3bb602c7f 100644 --- a/guacamole/src/main/frontend/src/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/frontend/src/app/clipboard/styles/clipboard.css @@ -58,12 +58,12 @@ overflow: hidden; } -.active { +#clipboard-settings .clipboard.active { overflow: auto; font-size: 1em; } -.inactive { +#clipboard-settings .clipboard.inactive { overflow: hidden; font-size: 0.9em; opacity: 0.5; From d0574f8d82c47edc2cfe21225839deeceb52b8a3 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Mar 2023 16:34:38 -0800 Subject: [PATCH 127/289] GUACAMOLE-839: Use BouncyCastle for retrieval of certificate details. Java's build-in support for reading X.509 certificates does not deal well with PIV certificates containing the username as a "serialNumber" attribute. Rather than exposing the string value of that attribute, the Java implementation exposes a byte array that does not fully match the string value shown by a tool like OpenSSL. BouncyCastle, on the other hand, _does_ match the output of OpenSSL, and provides a predictable means of decoding the certificate. --- .../bouncycastle-pkix-fips-1.0.7/LICENSE | 20 +++++++++++ .../bouncycastle-pkix-fips-1.0.7/README | 8 +++++ .../dep-coordinates.txt | 1 + .../modules/guacamole-auth-sso-ssl/pom.xml | 7 ++++ .../ssl/SSLClientAuthenticationResource.java | 36 +++++++++---------- 5 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE create mode 100644 doc/licenses/bouncycastle-pkix-fips-1.0.7/README create mode 100644 doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt diff --git a/doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE b/doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE new file mode 100644 index 000000000..a02bc176b --- /dev/null +++ b/doc/licenses/bouncycastle-pkix-fips-1.0.7/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2000 - 2021 The Legion of the Bouncy Castle Inc. +(https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/bouncycastle-pkix-fips-1.0.7/README b/doc/licenses/bouncycastle-pkix-fips-1.0.7/README new file mode 100644 index 000000000..b362257b8 --- /dev/null +++ b/doc/licenses/bouncycastle-pkix-fips-1.0.7/README @@ -0,0 +1,8 @@ +BouncyCastle PKIX APIs, FIPS Distribution (https://www.bouncycastle.org/fips-java) +----------------------------------------------------------------------- + + Version: 1.0.7 + From: 'The Legion of Bouncy Castle' (https://www.bouncycastle.org) + License(s): + MIT (bundled/bouncycastle-pkix-fips-1.0.7/LICENSE) + diff --git a/doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt b/doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt new file mode 100644 index 000000000..23ca14e38 --- /dev/null +++ b/doc/licenses/bouncycastle-pkix-fips-1.0.7/dep-coordinates.txt @@ -0,0 +1 @@ +org.bouncycastle:bcpkix-fips:jar:1.0.7 diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml index c6542ae56..7ece6b40e 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/pom.xml @@ -119,6 +119,13 @@ jsr311-api
    + + + org.bouncycastle + bcpkix-fips + 1.0.7 + + diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 21a2e0c25..75a190d35 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -19,17 +19,14 @@ package org.apache.guacamole.auth.ssl; import com.google.inject.Inject; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.security.Principal; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; +import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; import javax.naming.InvalidNameException; @@ -46,9 +43,14 @@ import javax.ws.rs.core.UriBuilder; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; +import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.auth.sso.NonceService; import org.apache.guacamole.auth.sso.SSOResource; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.openssl.PEMParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -247,27 +249,25 @@ public class SSLClientAuthenticationResource extends SSOResource { public String getUsername(byte[] certificate) throws GuacamoleException { // Parse and re-verify certificate is valid with respect to timestamps - X509Certificate cert; - try (InputStream input = new ByteArrayInputStream(certificate)) { + X509CertificateHolder cert; + try (Reader reader = new StringReader(new String(certificate, StandardCharsets.UTF_8))) { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - cert = (X509Certificate) certFactory.generateCertificate(input); + PEMParser parser = new PEMParser(reader); + cert = (X509CertificateHolder) parser.readObject(); // Verify certificate is valid (it should be given pre-validation // from SSL termination, but it's worth rechecking for sanity) - cert.checkValidity(); + if (!cert.isValidOn(new Date())) + throw new GuacamoleClientException("Certificate has expired."); } - catch (CertificateException e) { - throw new GuacamoleClientException("Certificate is not valid: " + e.getMessage(), e); - } catch (IOException e) { - throw new GuacamoleClientException("Certificate could not be read: " + e.getMessage(), e); + throw new GuacamoleServerException("Certificate could not be read: " + e.getMessage(), e); } - // Extract user's DN from their X.509 certificate - Principal principal = cert.getSubjectX500Principal(); - return getUsername(principal.getName()); + // Extract user's DN from their X.509 certificate in LDAP (RFC 4919) format + X500Name subject = X500Name.getInstance(RFC4519Style.INSTANCE, cert.getSubject()); + return getUsername(subject.toString()); } From 9f8bb71b0ef7cb41b7042456f2571c7882bbb03e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Mar 2023 16:37:00 -0800 Subject: [PATCH 128/289] GUACAMOLE-839: Correct JavaDoc for parameters of getUsername(). --- .../auth/ssl/SSLClientAuthenticationResource.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 75a190d35..583f2caa9 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -272,16 +272,13 @@ public class SSLClientAuthenticationResource extends SSOResource { } /** - * Processes the X.509 certificate in the headers of the given HTTP - * request, returning an authentication session token representing the + * Processes the X.509 certificate in the given set of HTTP request + * headers, returning an authentication session token representing the * identity in that certificate. If the certificate is invalid or not * present, an invalid session token is returned. * - * @param credentials - * The credentials submitted in the HTTP request being processed. - * - * @param request - * The HTTP request to process. + * @param headers + * The headers of the HTTP request to process. * * @return * An authentication session token representing the identity in the From 82073a5976715933a5d1085b8a6a9b057936efd9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 7 Mar 2023 16:38:51 -0800 Subject: [PATCH 129/289] GUACAMOLE-839: Correct typo in JavaDoc of decode() - "valid", not "value". --- .../guacamole/auth/ssl/SSLClientAuthenticationResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 583f2caa9..787bf3e52 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -149,7 +149,7 @@ public class SSLClientAuthenticationResource extends SSOResource { * The decoded string. * * @throws GuacamoleException - * If the provided value is not a value URL-encoded string. + * If the provided value is not a valid URL-encoded string. */ private byte[] decode(String value) throws GuacamoleException { try { From 6424b063f28c7de7526b792b3ea1a0ae946ef9b2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 8 Mar 2023 09:34:26 -0800 Subject: [PATCH 130/289] GUACAMOLE-839: Correct WildcardURIGuacamoleProperty to correctly handle missing (null) properties. --- .../guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java index d237d8031..ab08ee308 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/conf/WildcardURIGuacamoleProperty.java @@ -45,6 +45,9 @@ public abstract class WildcardURIGuacamoleProperty extends URIGuacamoleProperty @Override public URI parseValue(String value) throws GuacamoleException { + if (value == null) + return null; + // Verify wildcard prefix is present Matcher matcher = WILDCARD_URI_PATTERN.matcher(value); if (matcher.matches()) { From f98901f933a6601dc9865d61e8423cd0cebaed02 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 8 Mar 2023 09:34:52 -0800 Subject: [PATCH 131/289] GUACAMOLE-839: Add sanity checks around parsed PEM data, which may indeed be null. --- .../auth/ssl/SSLClientAuthenticationResource.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java index 787bf3e52..984a68f34 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/SSLClientAuthenticationResource.java @@ -253,7 +253,20 @@ public class SSLClientAuthenticationResource extends SSOResource { try (Reader reader = new StringReader(new String(certificate, StandardCharsets.UTF_8))) { PEMParser parser = new PEMParser(reader); - cert = (X509CertificateHolder) parser.readObject(); + Object object = parser.readObject(); + + // Verify received data is indeed an X.509 certificate + if (object == null || !(object instanceof X509CertificateHolder)) + throw new GuacamoleClientException("Certificate did not " + + "contain an X.509 certificate."); + + // Verify sanity of received certificate (there should be only + // one object here) + if (parser.readObject() != null) + throw new GuacamoleClientException("Certificate contains " + + "more than a single X.509 certificate."); + + cert = (X509CertificateHolder) object; // Verify certificate is valid (it should be given pre-validation // from SSL termination, but it's worth rechecking for sanity) From d5e19d5e76bd574d81dc8e126a08d3014663c9ec Mon Sep 17 00:00:00 2001 From: Niubbo75 Date: Mon, 8 Aug 2022 14:26:41 +0200 Subject: [PATCH 132/289] GUACAMOLE-1612: Update and correct Italian Translations for Guacamole Client --- .../src/main/resources/translations/it.json | 5 - .../src/main/resources/translations/it.json | 1 - .../src/main/resources/translations/it.json | 15 -- .../src/main/resources/translations/it.json | 4 - .../main/frontend/src/translations/it.json | 212 +++++++++--------- 5 files changed, 106 insertions(+), 131 deletions(-) diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/it.json b/extensions/guacamole-auth-duo/src/main/resources/translations/it.json index a16710c7c..d7bb5a7c1 100644 --- a/extensions/guacamole-auth-duo/src/main/resources/translations/it.json +++ b/extensions/guacamole-auth-duo/src/main/resources/translations/it.json @@ -1,11 +1,6 @@ { - "DATA_SOURCE_DUO" : { - "NAME" : "Duo TFA Backend" - }, - "LOGIN" : { - "FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "", "INFO_DUO_VALIDATION_CODE_INCORRECT" : "Codice di convalida Duo errato.", "INFO_DUO_AUTH_REQUIRED" : "Si prega di autenticarsi con Duo per continuare." } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json index c4a4e0305..092e9cfbd 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/it.json @@ -2,7 +2,6 @@ "LOGIN" : { - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", "ERROR_PASSWORD_SAME" : "La nuova password deve essere diversa dalla password scaduta.", "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", "ERROR_NOT_VALID" : "Questo account utente non è attualmente valido.", diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json index 246029367..bafa8c347 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/it.json @@ -1,21 +1,6 @@ { - "DATA_SOURCE_CAS" : { - "NAME" : "CAS SSO Backend" - }, - - "DATA_SOURCE_OPENID" : { - "NAME" : "OpenID SSO Backend" - }, - - "DATA_SOURCE_SAML" : { - "NAME" : "SAML SSO Backend" - }, - "LOGIN" : { - "FIELD_HEADER_ID_TOKEN" : "", - "FIELD_HEADER_STATE" : "", - "FIELD_HEADER_TICKET" : "", "INFO_IDP_REDIRECT_PENDING" : "Attendi, reindirizzamento al provider di identità...", "NAME_IDP_CAS" : "CAS", "NAME_IDP_OPENID" : "OpenID", diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/it.json b/extensions/guacamole-auth-totp/src/main/resources/translations/it.json index 97b30f469..a2fa7903b 100644 --- a/extensions/guacamole-auth-totp/src/main/resources/translations/it.json +++ b/extensions/guacamole-auth-totp/src/main/resources/translations/it.json @@ -4,10 +4,6 @@ "NAME" : "TOTP TFA Backend" }, - "LOGIN" : { - "FIELD_HEADER_GUAC_TOTP" : "" - }, - "TOTP" : { "ACTION_HIDE_DETAILS" : "Nascondere", diff --git a/guacamole/src/main/frontend/src/translations/it.json b/guacamole/src/main/frontend/src/translations/it.json index 64aad1f8a..7dbf24db7 100644 --- a/guacamole/src/main/frontend/src/translations/it.json +++ b/guacamole/src/main/frontend/src/translations/it.json @@ -62,10 +62,10 @@ "ERROR_CLIENT_303" : "Non hai i permessi per accedere a questa connessione. Se ne hai bisogno, chiedi ai tuoi amministratori di sistema di aggiungerti agli utenti abilitati.", "ERROR_CLIENT_308" : "Il server Guacamole ha chiuso la connessione perchè il tuo browser non ha risposto per troppo tempo e sembrava essersi disconnesso. Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", "ERROR_CLIENT_31D" : "Il server Guacamole rifiuta l'accesso alla connessione perchè hai raggiunto il limite massimo di connessioni simultanee per singolo utente. Chiudi una o più connessioni e riprova.", - "ERROR_CLIENT_DEFAULT" : "Si è verificato un errore interno sul server Guacamole, e la connessione è stata terminata. Se il problema persiste avvisa il tuo amministratore di sistema o controlla i file di log.", + "ERROR_CLIENT_DEFAULT" : "Si è verificato un errore interno sul server Guacamole, la connessione è stata terminata. Se il problema persiste avvisa il tuo amministratore di sistema o controlla i file di log.", "ERROR_TUNNEL_201" : "Il server Guacamole ha rifiutato questa connessione perchè ci sono troppe connessioni attive. Attendi qualche minuto e riprova.", - "ERROR_TUNNEL_202" : "La connessione è stata chiusa dal server is taking too long to respond.Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", + "ERROR_TUNNEL_202" : "La connessione è stata chiusa dal server perché ci vuole troppo tempo per rispondere. Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", "ERROR_TUNNEL_203" : "Si è verificato un errore sul server e la connessione è stata chiusa. Riprova o contatta il tuo amministratore di sistema.", "ERROR_TUNNEL_204" : "La connessione richiesta non esiste. Controlla il nome della connessione e riprova. Grazie.", "ERROR_TUNNEL_205" : "Questa connessione è già in uso, non sono possibili accessi concorrenti. Riprova più tardi. Grazie.", @@ -80,7 +80,7 @@ "ERROR_UPLOAD_202" : "Trasferimento annullato: il desktop remoto risponde troppo lentamente. Per favore riprova o contatta il tuo amministratore di sistema.", "ERROR_UPLOAD_203" : "Il desktop remoto ha incontrato un errore durante il trasferimento. Per favore riprova o contatta il tuo amministratore di sistema.", "ERROR_UPLOAD_204" : "La destinazione del file non esiste. Controlla che la destinazione esista e riprova.", - "ERROR_UPLOAD_205" : "La destinazione del file non è bloccata. Per favore attendi che ogni processo in corso sia termianto e riprova.", + "ERROR_UPLOAD_205" : "La destinazione del file è bloccata. Per favore attendi che ogni processo in corso sia terminato e riprova.", "ERROR_UPLOAD_301" : "Non hai i permessi per caricare i file perchè non hai fatto il login. Inserisci nome utente e password e riprova.", "ERROR_UPLOAD_303" : "Non hai i permessi per caricare i file. Se ti serve questa funzionalità contatta il tuo amministratore di sistema.", "ERROR_UPLOAD_308" : "Il trasferimento di file si è fermato. Solitamente la causa è un problema di rete, ad esempio un segnale wifi instabile o una connesisone molto lenta. Controlla la tua rete e riprova.", @@ -91,7 +91,7 @@ "HELP_CLIPBOARD" : "Il testo copiato/tagliato appare qui. I cambiamenti effettuati al testo qui sotto saranno riportati negli appunti remoti.", "HELP_INPUT_METHOD_NONE" : "Non c'è nessun metodo di immissione. L'input da tastiera è accettato da una tastiera fisica connessa.", - "HELP_INPUT_METHOD_OSK" : "Mostra e accetta input dalla tastiera su schermo. La tastiera su schermo ti permette di scrivere combinazioni di tasti altrimenti mipossibli (ad esempio Ctrl-Alt-Canc).", + "HELP_INPUT_METHOD_OSK" : "Mostra e accetta input dalla tastiera su schermo. La tastiera su schermo ti permette di scrivere combinazioni di tasti altrimenti impossibli (ad esempio Ctrl-Alt-Canc).", "HELP_INPUT_METHOD_TEXT" : "Abilita la battitura di testo ed emula gli eventi da tastiera basati sul testo battuto. Questo è necessario per tablet e smartphone che non hanno una tastiera fisica.", "HELP_MOUSE_MODE" : "Determina come si deve comportare il mouse remoto in base al touch.", "HELP_MOUSE_MODE_ABSOLUTE" : "Tap to click. Il click è sostituito dal tocco.", @@ -130,8 +130,8 @@ "FORM" : { - "HELP_SHOW_PASSWORD" : "Click to show password", - "HELP_HIDE_PASSWORD" : "Click to hide password" + "HELP_SHOW_PASSWORD" : "Fare clic per mostrare la password", + "HELP_HIDE_PASSWORD" : "Fare clic per nascondere la password" }, @@ -160,7 +160,7 @@ "ERROR_INVALID_LOGIN" : "Nome utente e/o password errati.", - "FIELD_HEADER_USERNAME" : "Username", + "FIELD_HEADER_USERNAME" : "Nome utente", "FIELD_HEADER_PASSWORD" : "Password" }, @@ -176,9 +176,9 @@ "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina connessione", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - "FIELD_HEADER_LOCATION" : "Location:", - "FIELD_HEADER_NAME" : "Name:", - "FIELD_HEADER_PROTOCOL" : "Protocol:", + "FIELD_HEADER_LOCATION" : "Posizione:", + "FIELD_HEADER_NAME" : "Nome:", + "FIELD_HEADER_PROTOCOL" : "Protocollo:", "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", @@ -190,8 +190,8 @@ "SECTION_HEADER_HISTORY" : "Cronologia di utilizzo", "SECTION_HEADER_PARAMETERS" : "Parametri", - "TABLE_HEADER_HISTORY_USERNAME" : "Username", - "TABLE_HEADER_HISTORY_START" : "Start Time", + "TABLE_HEADER_HISTORY_USERNAME" : "Nome utente", + "TABLE_HEADER_HISTORY_START" : "Ora di inizio", "TABLE_HEADER_HISTORY_DURATION" : "Durata", "TEXT_CONFIRM_DELETE" : "Le Connessioni non possono essere ripristinate dopo la loro eliminazione. Sei sicuro di volere eliminare questa connessione?" @@ -208,7 +208,7 @@ "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina Gruppo di Connesisoni", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - "FIELD_HEADER_LOCATION" : "Location:", + "FIELD_HEADER_LOCATION" : "Posizione:", "FIELD_HEADER_NAME" : "Nome:", "FIELD_HEADER_TYPE" : "Tipo:", @@ -231,7 +231,7 @@ "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - "FIELD_HEADER_NAME" : "Name:", + "FIELD_HEADER_NAME" : "Nome:", "SECTION_HEADER_PARAMETERS" : "Parametri" @@ -257,7 +257,7 @@ "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Crea un ruppo di connessioni:", "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", - "FIELD_HEADER_USERNAME" : "Username:", + "FIELD_HEADER_USERNAME" : "Nome utente:", "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", @@ -271,44 +271,44 @@ "PROTOCOL_RDP" : { - "FIELD_HEADER_COLOR_DEPTH" : "Color depth:", - "FIELD_HEADER_CONSOLE" : "Administrator console:", - "FIELD_HEADER_CONSOLE_AUDIO" : "Support audio in console:", + "FIELD_HEADER_COLOR_DEPTH" : "Profondità di colore:", + "FIELD_HEADER_CONSOLE" : "Console amministratore:", + "FIELD_HEADER_CONSOLE_AUDIO" : "Supporta l'audio nella console:", "FIELD_HEADER_CLIENT_NAME" : "Client name:", - "FIELD_HEADER_DISABLE_AUDIO" : "Disable audio:", - "FIELD_HEADER_DISABLE_AUTH" : "Disable authentication:", - "FIELD_HEADER_DOMAIN" : "Domain:", - "FIELD_HEADER_DPI" : "Resolution (DPI):", + "FIELD_HEADER_DISABLE_AUDIO" : "Disattiva audio:", + "FIELD_HEADER_DISABLE_AUTH" : "Disabilita autenticazione:", + "FIELD_HEADER_DOMAIN" : "Dominio:", + "FIELD_HEADER_DPI" : "Risoluzione (DPI):", "FIELD_HEADER_DRIVE_PATH" : "Drive path:", - "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Enable desktop composition (Aero):", - "FIELD_HEADER_ENABLE_DRIVE" : "Enable drive:", - "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Enable font smoothing (ClearType):", - "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Enable full-window drag:", - "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Enable menu animations:", - "FIELD_HEADER_ENABLE_PRINTING" : "Enable printing:", - "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", - "FIELD_HEADER_ENABLE_THEMING" : "Enable theming:", - "FIELD_HEADER_ENABLE_WALLPAPER" : "Enable wallpaper:", - "FIELD_HEADER_HEIGHT" : "Height:", - "FIELD_HEADER_HOSTNAME" : "Hostname:", - "FIELD_HEADER_IGNORE_CERT" : "Ignore server certificate:", - "FIELD_HEADER_INITIAL_PROGRAM" : "Initial program:", + "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Abilita composizione desktop (Aero):", + "FIELD_HEADER_ENABLE_DRIVE" : "Abilita drive:", + "FIELD_HEADER_ENABLE_FONT_SMOOTHING" : "Abilita l'arrotondamento dei caratteri (ClearType):", + "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG" : "Abilita il trascinamento della finestra intera:", + "FIELD_HEADER_ENABLE_MENU_ANIMATIONS" : "Abilita le animazioni del menu:", + "FIELD_HEADER_ENABLE_PRINTING" : "Abilita la stampa:", + "FIELD_HEADER_ENABLE_SFTP" : "Abilita SFTP:", + "FIELD_HEADER_ENABLE_THEMING" : "Abilita temi:", + "FIELD_HEADER_ENABLE_WALLPAPER" : "Abilita sfondo:", + "FIELD_HEADER_HEIGHT" : "Altezza:", + "FIELD_HEADER_HOSTNAME" : "Nome host:", + "FIELD_HEADER_IGNORE_CERT" : "Ignora certificato server:", + "FIELD_HEADER_INITIAL_PROGRAM" : "Programma iniziale:", "FIELD_HEADER_PASSWORD" : "Password:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_REMOTE_APP_ARGS" : "Parameters:", - "FIELD_HEADER_REMOTE_APP_DIR" : "Working directory:", - "FIELD_HEADER_REMOTE_APP" : "Program:", - "FIELD_HEADER_SECURITY" : "Security mode:", - "FIELD_HEADER_SERVER_LAYOUT" : "Keyboard layout:", - "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", + "FIELD_HEADER_PORT" : "Porta:", + "FIELD_HEADER_REMOTE_APP_ARGS" : "Parametri:", + "FIELD_HEADER_REMOTE_APP_DIR" : "Directory di lavoro:", + "FIELD_HEADER_REMOTE_APP" : "Programma:", + "FIELD_HEADER_SECURITY" : "Modalità di sicurezza:", + "FIELD_HEADER_SERVER_LAYOUT" : "Layout della tastiera:", + "FIELD_HEADER_SFTP_HOSTNAME" : "Nome host:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Password:", - "FIELD_HEADER_SFTP_PORT" : "Port:", - "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Private key:", - "FIELD_HEADER_SFTP_USERNAME" : "Username:", - "FIELD_HEADER_STATIC_CHANNELS" : "Static channel names:", - "FIELD_HEADER_USERNAME" : "Username:", - "FIELD_HEADER_WIDTH" : "Width:", + "FIELD_HEADER_SFTP_PORT" : "Porta:", + "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Chiave privata:", + "FIELD_HEADER_SFTP_USERNAME" : "Nome utente:", + "FIELD_HEADER_STATIC_CHANNELS" : "Nomi dei canali statici:", + "FIELD_HEADER_USERNAME" : "Nome utente:", + "FIELD_HEADER_WIDTH" : "Larghezza:", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", @@ -316,7 +316,7 @@ "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", - "FIELD_OPTION_SECURITY_ANY" : "Any", + "FIELD_OPTION_SECURITY_ANY" : "Qualsiasi", "FIELD_OPTION_SECURITY_EMPTY" : "", "FIELD_OPTION_SECURITY_NLA" : "NLA (Network Level Authentication)", "FIELD_OPTION_SECURITY_RDP" : "RDP encryption", @@ -327,19 +327,19 @@ "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US English (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE" : "Unicode", "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)", - "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)", + "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italiana (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_PL_PL_QWERTY" : "Polacca (Qwerty)", "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)", "NAME" : "RDP", - "SECTION_HEADER_AUTHENTICATION" : "Authentication", - "SECTION_HEADER_BASIC_PARAMETERS" : "Basic Settings", + "SECTION_HEADER_AUTHENTICATION" : "Autenticazione", + "SECTION_HEADER_BASIC_PARAMETERS" : "Impostazioni di base", "SECTION_HEADER_CLIPBOARD" : "Appunti", - "SECTION_HEADER_DEVICE_REDIRECTION" : "Device Redirection", + "SECTION_HEADER_DEVICE_REDIRECTION" : "Reindirizzamento del dispositivo", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", - "SECTION_HEADER_PERFORMANCE" : "Performance", + "SECTION_HEADER_PERFORMANCE" : "Prestazione", "SECTION_HEADER_REMOTEAPP" : "RemoteApp", "SECTION_HEADER_SFTP" : "SFTP" @@ -347,15 +347,15 @@ "PROTOCOL_SSH" : { - "FIELD_HEADER_FONT_NAME" : "Font name:", - "FIELD_HEADER_FONT_SIZE" : "Font size:", - "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", - "FIELD_HEADER_HOSTNAME" : "Hostname:", - "FIELD_HEADER_USERNAME" : "Username:", + "FIELD_HEADER_FONT_NAME" : "Nome carattere:", + "FIELD_HEADER_FONT_SIZE" : "Dimensione del carattere:", + "FIELD_HEADER_ENABLE_SFTP" : "Abilita SFTP:", + "FIELD_HEADER_HOSTNAME" : "Nome host:", + "FIELD_HEADER_USERNAME" : "Nome utente:", "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSPHRASE" : "Passphrase:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_PRIVATE_KEY" : "Private key:", + "FIELD_HEADER_PORT" : "Porta:", + "FIELD_HEADER_PRIVATE_KEY" : "Chiave privata:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", @@ -375,7 +375,7 @@ "NAME" : "SSH", - "SECTION_HEADER_AUTHENTICATION" : "Authentication", + "SECTION_HEADER_AUTHENTICATION" : "Autenticazione", "SECTION_HEADER_CLIPBOARD" : "Appunti", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", @@ -385,13 +385,13 @@ "PROTOCOL_TELNET" : { - "FIELD_HEADER_FONT_NAME" : "Font name:", - "FIELD_HEADER_FONT_SIZE" : "Font size:", - "FIELD_HEADER_HOSTNAME" : "Hostname:", - "FIELD_HEADER_USERNAME" : "Username:", + "FIELD_HEADER_FONT_NAME" : "Nome carattere:", + "FIELD_HEADER_FONT_SIZE" : "Dimensione del carattere:", + "FIELD_HEADER_HOSTNAME" : "Nome host:", + "FIELD_HEADER_USERNAME" : "Nome utente:", "FIELD_HEADER_PASSWORD" : "Password:", - "FIELD_HEADER_PASSWORD_REGEX" : "Password regular expression:", - "FIELD_HEADER_PORT" : "Port:", + "FIELD_HEADER_PASSWORD_REGEX" : "Espressione regolare della password:", + "FIELD_HEADER_PORT" : "Porta:", "FIELD_OPTION_FONT_SIZE_8" : "8", "FIELD_OPTION_FONT_SIZE_9" : "9", @@ -411,7 +411,7 @@ "NAME" : "Telnet", - "SECTION_HEADER_AUTHENTICATION" : "Authentication", + "SECTION_HEADER_AUTHENTICATION" : "Autenticazione", "SECTION_HEADER_CLIPBOARD" : "Appunti", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network" @@ -420,25 +420,25 @@ "PROTOCOL_VNC" : { - "FIELD_HEADER_AUDIO_SERVERNAME" : "Audio server name:", - "FIELD_HEADER_COLOR_DEPTH" : "Color depth:", - "FIELD_HEADER_CURSOR" : "Cursor:", - "FIELD_HEADER_DEST_HOST" : "Destination host:", - "FIELD_HEADER_DEST_PORT" : "Destination port:", - "FIELD_HEADER_ENABLE_AUDIO" : "Enable audio:", - "FIELD_HEADER_ENABLE_SFTP" : "Enable SFTP:", - "FIELD_HEADER_HOSTNAME" : "Hostname:", - "FIELD_HEADER_USERNAME" : "Username:", + "FIELD_HEADER_AUDIO_SERVERNAME" : "Nome del server audio:", + "FIELD_HEADER_COLOR_DEPTH" : "Profondità di colore:", + "FIELD_HEADER_CURSOR" : "Cursore:", + "FIELD_HEADER_DEST_HOST" : "Host di destinazione:", + "FIELD_HEADER_DEST_PORT" : "Porto di destinazione:", + "FIELD_HEADER_ENABLE_AUDIO" : "Abilita audio:", + "FIELD_HEADER_ENABLE_SFTP" : "Abilita SFTP:", + "FIELD_HEADER_HOSTNAME" : "Nome host:", + "FIELD_HEADER_USERNAME" : "Nome utente:", "FIELD_HEADER_PASSWORD" : "Password:", - "FIELD_HEADER_PORT" : "Port:", - "FIELD_HEADER_READ_ONLY" : "Read-only:", - "FIELD_HEADER_SFTP_HOSTNAME" : "Hostname:", + "FIELD_HEADER_PORT" : "Porta:", + "FIELD_HEADER_READ_ONLY" : "Sola lettura:", + "FIELD_HEADER_SFTP_HOSTNAME" : "Nome host:", "FIELD_HEADER_SFTP_PASSPHRASE" : "Passphrase:", "FIELD_HEADER_SFTP_PASSWORD" : "Password:", - "FIELD_HEADER_SFTP_PORT" : "Port:", - "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Private key:", - "FIELD_HEADER_SFTP_USERNAME" : "Username:", - "FIELD_HEADER_SWAP_RED_BLUE" : "Swap red/blue components:", + "FIELD_HEADER_SFTP_PORT" : "Porta:", + "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Chiave privata:", + "FIELD_HEADER_SFTP_USERNAME" : "Nome utente:", + "FIELD_HEADER_SWAP_RED_BLUE" : "Scambia componenti rosso/blu:", "FIELD_OPTION_COLOR_DEPTH_8" : "256 color", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", @@ -447,13 +447,13 @@ "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "", "FIELD_OPTION_CURSOR_EMPTY" : "", - "FIELD_OPTION_CURSOR_LOCAL" : "Local", - "FIELD_OPTION_CURSOR_REMOTE" : "Remote", + "FIELD_OPTION_CURSOR_LOCAL" : "Locale", + "FIELD_OPTION_CURSOR_REMOTE" : "Remoto", "NAME" : "VNC", "SECTION_HEADER_AUDIO" : "Audio", - "SECTION_HEADER_AUTHENTICATION" : "Authentication", + "SECTION_HEADER_AUTHENTICATION" : "Autenticazione", "SECTION_HEADER_CLIPBOARD" : "Appunti", "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_NETWORK" : "Network", @@ -464,7 +464,7 @@ "SETTINGS" : { - "SECTION_HEADER_SETTINGS" : "Settings" + "SECTION_HEADER_SETTINGS" : "Impostazioni" }, @@ -478,7 +478,7 @@ "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - "HELP_CONNECTIONS" : "Fai click o tap sulla connessione qui sotto per gestire quella connessione. In base al tuo livello di accesso, le connessioni possono essere craete, eliminate, e le relative proprietà (protocol, hostname, port, etc.) possono essere cambiate.", + "HELP_CONNECTIONS" : "Fai click o tap sulla connessione qui sotto per gestire quella connessione. In base al tuo livello di accesso, le connessioni possono essere create, eliminate, e le relative proprietà (protocol, hostname, port, etc.) possono essere cambiate.", "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", @@ -495,9 +495,9 @@ "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nome della connessione", - "TABLE_HEADER_SESSION_STARTDATE" : "Start Time", + "TABLE_HEADER_SESSION_STARTDATE" : "Ora di inizio", "TABLE_HEADER_SESSION_DURATION" : "Durata", - "TABLE_HEADER_SESSION_USERNAME" : "Username" + "TABLE_HEADER_SESSION_USERNAME" : "Nome utente" }, @@ -517,17 +517,17 @@ "FIELD_HEADER_PASSWORD_OLD" : "Password Attuale:", "FIELD_HEADER_PASSWORD_NEW" : "Nuova Password:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Conferma Nuova Password:", - "FIELD_HEADER_USERNAME" : "Username:", + "FIELD_HEADER_USERNAME" : "Nome utente", - "HELP_DEFAULT_INPUT_METHOD" : "The default input method determines how keyboard events are received by Guacamole. Changing this setting may be necessary when using a mobile device, or when typing through an IME. This setting can be overridden on a per-connection basis within the Guacamole menu.", - "HELP_DEFAULT_MOUSE_MODE" : "The default mouse emulation mode determines how the remote mouse will behave in new connections with respect to touches. This setting can be overridden on a per-connection basis within the Guacamole menu.", + "HELP_DEFAULT_INPUT_METHOD" : "Il metodo di input predefinito determina come gli eventi della tastiera vengono ricevuti da Guacamole. La modifica di questa impostazione potrebbe essere necessaria quando si utilizza un dispositivo mobile o quando si digita un IME. Questa impostazione può essere ignorata in base alla connessione all'interno del menu Guacamole.", + "HELP_DEFAULT_MOUSE_MODE" : "La modalità di emulazione del mouse predefinita determina come si comporterà il mouse remoto nelle nuove connessioni rispetto ai tocchi. Questa impostazione può essere ignorata in base alla connessione all'interno del menu Guacamole.", "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", - "HELP_LANGUAGE" : "Select a different language below to change the language of all text within Guacamole. Available choices will depend on which languages are installed.", + "HELP_LANGUAGE" : "Seleziona una lingua diversa di seguito per cambiare la lingua di tutto il testo all'interno di Guacamole. Le scelte disponibili dipenderanno dalle lingue installate.", "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", - "HELP_UPDATE_PASSWORD" : "Se desideri cambiare la tua password, inserisci la tua password attuale e sotto scriti quella che desideri come nuova password, clicca \"Modifica Password\". La modifica avrà effetto immediato.", + "HELP_UPDATE_PASSWORD" : "Se desideri cambiare la tua password, inserisci la tua password attuale e sotto scrivi quella che desideri come nuova password, clicca \"Modifica Password\". La modifica avrà effetto immediato.", "INFO_PASSWORD_CHANGED" : "Password Modificata.", @@ -535,8 +535,8 @@ "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", - "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Default Input Method", - "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Default Mouse Emulation Mode", + "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Metodo di immissione predefinito", + "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Modalità di emulazione del mouse predefinita", "SECTION_HEADER_UPDATE_PASSWORD" : "Modifica Password" }, @@ -552,11 +552,11 @@ "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", - "HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.", + "HELP_USERS" : "Fare clic o toccare un utente di seguito per gestire quell'utente. A seconda del livello di accesso, gli utenti possono essere aggiunti ed eliminati e le loro password possono essere modificate.", "SECTION_HEADER_USERS" : "Utenti", - "TABLE_HEADER_USERNAME" : "Username" + "TABLE_HEADER_USERNAME" : "Nome utente" }, @@ -564,7 +564,7 @@ "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_DELETE" : "Termian Sessione", + "ACTION_DELETE" : "Termina Sessione", "DIALOG_HEADER_CONFIRM_DELETE" : "Termina Sessione", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", @@ -573,18 +573,18 @@ "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", - "HELP_SESSIONS" : "Questa pagina verrà popolata con connessioni attualmente attive. Le connessioni elencate e la possibilità di uccidere tali connessioni dipende dal tuo livello di accesso. Se desideri uccidere una o più sessioni, seleziona la casella accanto a quelle sessioni e fai clic su \"Uccidi sessioni \". L'uccisione di una sessione interromperà immediatamente l'utente dalla connessione associata.", + "HELP_SESSIONS" : "Questa pagina verrà popolata con connessioni attualmente attive. Le connessioni elencate e la possibilità di terminare tali connessioni dipende dal tuo livello di accesso. Se desideri terminare una o più sessioni, seleziona la casella accanto a quelle sessioni e fai clic su \"Termina sessione \". Terminando una sessione si disconnetterà immediatamente l'utente dalla connessione associata.", "INFO_NO_SESSIONS" : "Nessuna sessione attiva", "SECTION_HEADER_SESSIONS" : "Sessioni Attive", - "TABLE_HEADER_SESSION_USERNAME" : "Username", + "TABLE_HEADER_SESSION_USERNAME" : "Nome utente", "TABLE_HEADER_SESSION_STARTDATE" : "Attivo da", - "TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host", + "TABLE_HEADER_SESSION_REMOTEHOST" : "Host remoto", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nome della connessione", - "TEXT_CONFIRM_DELETE" : "Sei sicuro di voler termianre la sessione selezionata? L'utente che sta utilizzando questa sessione sarà immediatamente disconnesso." + "TEXT_CONFIRM_DELETE" : "Sei sicuro di voler terminare la sessione selezionata? L'utente che sta utilizzando questa sessione sarà immediatamente disconnesso." }, From 4dbf35766ff47f266692969a4fae9b47c79221cc Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 13 Mar 2023 21:17:45 -0400 Subject: [PATCH 133/289] GUACAMOLE-1219: Add support for disabling TOTP for specific users and groups. --- .../guacamole/auth/totp/user/TOTPUser.java | 12 +- .../auth/totp/user/TOTPUserContext.java | 27 +++++ .../totp/user/UserVerificationService.java | 67 +++++++++++ .../auth/totp/usergroup/TOTPUserGroup.java | 104 ++++++++++++++++++ .../src/main/resources/translations/en.json | 9 ++ 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/usergroup/TOTPUserGroup.java diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java index 0c6f567b8..e84b7543b 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUser.java @@ -33,6 +33,12 @@ import org.apache.guacamole.net.auth.User; */ public class TOTPUser extends DelegatingUser { + /** + * The name of the user attribute which disables the TOTP requirement + * for that specific user. + */ + public static final String TOTP_KEY_DISABLED_ATTRIBUTE_NAME = "guac-totp-disabled"; + /** * The name of the user attribute which stores the TOTP key. */ @@ -56,13 +62,14 @@ public class TOTPUser extends DelegatingUser { * The string value used by TOTP user attributes to represent the boolean * value "true". */ - private static final String TRUTH_VALUE = "true"; + public static final String TRUTH_VALUE = "true"; /** * The form which contains all configurable properties for this user. */ public static final Form TOTP_ENROLLMENT_STATUS = new Form("totp-enrollment-status", Arrays.asList( + new BooleanField(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, TRUTH_VALUE), new BooleanField(TOTP_KEY_SECRET_GENERATED_ATTRIBUTE_NAME, TRUTH_VALUE), new BooleanField(TOTP_KEY_CONFIRMED_ATTRIBUTE_NAME, TRUTH_VALUE) ) @@ -95,6 +102,9 @@ public class TOTPUser extends DelegatingUser { // Create independent, mutable copy of attributes Map attributes = new HashMap<>(super.getAttributes()); + + if (!attributes.containsKey(TOTP_KEY_DISABLED_ATTRIBUTE_NAME)) + attributes.put(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, null); // Replace secret key with simple boolean attribute representing // whether a key has been generated at all diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java index a7a97b022..d3269e78a 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/TOTPUserContext.java @@ -23,12 +23,14 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.auth.totp.usergroup.TOTPUserGroup; import org.apache.guacamole.form.Form; import org.apache.guacamole.net.auth.DecoratingDirectory; import org.apache.guacamole.net.auth.DelegatingUserContext; import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.UserGroup; /** * TOTP-specific UserContext implementation which wraps the UserContext of @@ -65,11 +67,36 @@ public class TOTPUserContext extends DelegatingUserContext { }; } + @Override + public Directory getUserGroupDirectory() throws GuacamoleException { + return new DecoratingDirectory(super.getUserGroupDirectory()) { + + @Override + protected UserGroup decorate(UserGroup object) { + return new TOTPUserGroup(object); + } + + @Override + protected UserGroup undecorate(UserGroup object) { + assert(object instanceof TOTPUserGroup); + return ((TOTPUserGroup) object).getUndecorated(); + } + + }; + } + @Override public Collection getUserAttributes() { Collection userAttrs = new HashSet<>(super.getUserAttributes()); userAttrs.add(TOTPUser.TOTP_ENROLLMENT_STATUS); return Collections.unmodifiableCollection(userAttrs); } + + @Override + public Collection getUserGroupAttributes() { + Collection userGroupAttrs = new HashSet<>(super.getUserGroupAttributes()); + userGroupAttrs.add(TOTPUserGroup.TOTP_USER_GROUP_CONFIG); + return Collections.unmodifiableCollection(userGroupAttrs); + } } diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java index 2fca74229..027a228cd 100644 --- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/user/UserVerificationService.java @@ -26,19 +26,23 @@ import java.security.InvalidKeyException; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleUnsupportedException; import org.apache.guacamole.auth.totp.conf.ConfigurationService; import org.apache.guacamole.auth.totp.form.AuthenticationCodeField; +import org.apache.guacamole.auth.totp.usergroup.TOTPUserGroup; import org.apache.guacamole.form.Field; import org.apache.guacamole.language.TranslatableGuacamoleClientException; import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.totp.TOTPGenerator; import org.slf4j.Logger; @@ -203,6 +207,65 @@ public class UserVerificationService { return true; } + + /** + * Checks the user in question, via both UserContext and AuthenticatedUser, + * to see if TOTP has been disabled for this user, either directly or via + * membership in a group that has had TOTP marked as disabled. + * + * @param context + * The UserContext for the user being verified. + * + * @param authenticatedUser + * The AuthenticatedUser for the user being verified. + * + * @return + * True if TOTP access has been disabled for the user, otherwise + * false. + * + * @throws GuacamoleException + * If the extension handling storage fails internally while attempting + * to update the user. + */ + private boolean totpDisabled(UserContext context, + AuthenticatedUser authenticatedUser) + throws GuacamoleException { + + // If TOTP is disabled for this user, return, allowing login to continue + Map myAttributes = context.self().getAttributes(); + if (myAttributes != null + && TOTPUser.TRUTH_VALUE.equals(myAttributes.get(TOTPUser.TOTP_KEY_DISABLED_ATTRIBUTE_NAME))) { + + logger.warn("TOTP validation has been disabled for user \"{}\"", + context.self().getIdentifier()); + return true; + + } + + // Check if any effective user groups have TOTP marked as disabled + Set userGroups = authenticatedUser.getEffectiveUserGroups(); + Directory directoryGroups = context.getPrivileged().getUserGroupDirectory(); + for (String userGroup : userGroups) { + UserGroup thisGroup = directoryGroups.get(userGroup); + if (thisGroup == null) + continue; + + Map grpAttributes = thisGroup.getAttributes(); + if (grpAttributes != null + && TOTPUserGroup.TRUTH_VALUE.equals(grpAttributes.get(TOTPUserGroup.TOTP_KEY_DISABLED_ATTRIBUTE_NAME))) { + + logger.warn("TOTP validation will be bypassed for user \"{}\"" + + " because it has been disabled for group \"{}\"", + context.self().getIdentifier(), userGroup); + return true; + + } + } + + // TOTP has not been disabled + return false; + + } /** * Verifies the identity of the given user using TOTP. If a authentication @@ -230,6 +293,10 @@ public class UserVerificationService { if (username.equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) return; + // Check if TOTP has been disabled for this user + if (totpDisabled(context, authenticatedUser)) + return; + // Ignore users which do not have an associated key UserTOTPKey key = getKey(context, username); if (key == null) diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/usergroup/TOTPUserGroup.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/usergroup/TOTPUserGroup.java new file mode 100644 index 000000000..5b6921bab --- /dev/null +++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/usergroup/TOTPUserGroup.java @@ -0,0 +1,104 @@ +/* + * 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.totp.usergroup; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.apache.guacamole.form.BooleanField; +import org.apache.guacamole.form.Form; +import org.apache.guacamole.net.auth.DelegatingUserGroup; +import org.apache.guacamole.net.auth.UserGroup; + +/** + * A UserGroup that wraps another UserGroup implementation, decorating it with + * attributes that control TOTP configuration for users that are members of that + * group. + */ +public class TOTPUserGroup extends DelegatingUserGroup { + + /** + * The attribute associated with a group that disables the TOTP requirement + * for any users that are a member of that group, or are members of any + * groups that are members of this group. + */ + public static final String TOTP_KEY_DISABLED_ATTRIBUTE_NAME = "guac-totp-disabled"; + + /** + * The string value used by TOTP user attributes to represent the boolean + * value "true". + */ + public static final String TRUTH_VALUE = "true"; + + /** + * The form that contains fields for configuring TOTP for members of this + * group. + */ + public static final Form TOTP_USER_GROUP_CONFIG = new Form("totp-user-group-config", + Arrays.asList( + new BooleanField(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, TRUTH_VALUE) + ) + ); + + /** + * Create a new instance of this user group implementation, wrapping the + * provided UserGroup. + * + * @param userGroup + * The UserGroup to be wrapped. + */ + public TOTPUserGroup(UserGroup userGroup) { + super(userGroup); + } + + /** + * Return the original UserGroup that this implementation is wrapping. + * + * @return + * The original UserGroup that this implementation wraps. + */ + public UserGroup getUndecorated() { + return getDelegateUserGroupGroup(); + } + + /** + * Returns whether or not TOTP has been disabled for members of this group. + * + * @return + * True if TOTP has been disabled for members of this group, otherwise + * false. + */ + public boolean totpDisabled() { + return (TRUTH_VALUE.equals(getAttributes().get(TOTP_KEY_DISABLED_ATTRIBUTE_NAME))); + } + + @Override + public Map getAttributes() { + + // Create a mutable copy of the attributes + Map attributes = new HashMap<>(super.getAttributes()); + + if (!attributes.containsKey(TOTP_KEY_DISABLED_ATTRIBUTE_NAME)) + attributes.put(TOTP_KEY_DISABLED_ATTRIBUTE_NAME, null); + + return attributes; + + } + +} diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json index 80bdaf06a..e9f4b7140 100644 --- a/extensions/guacamole-auth-totp/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-totp/src/main/resources/translations/en.json @@ -33,11 +33,20 @@ "USER_ATTRIBUTES" : { + "FIELD_HEADER_GUAC_TOTP_DISABLED" : "Disable TOTP:", "FIELD_HEADER_GUAC_TOTP_KEY_GENERATED" : "Secret key generated:", "FIELD_HEADER_GUAC_TOTP_KEY_CONFIRMED" : "Authentication device confirmed:", "SECTION_HEADER_TOTP_ENROLLMENT_STATUS" : "TOTP Enrollment Status" + }, + + "USER_GROUP_ATTRIBUTES" : { + + "FIELD_HEADER_GUAC_TOTP_DISABLED" : "Disable TOTP:", + + "SECTION_HEADER_TOTP_USER_GROUP_CONFIG" : "TOTP Configuration" + } } From c21ee0f8301c8b3ec134be2bf9f9243b59e7c3c1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 22 Mar 2023 14:08:22 -0700 Subject: [PATCH 134/289] GUACAMOLE-1756: Display login failures regardless of whether the interactive login form was used. --- .../app/index/controllers/indexController.js | 25 ++++++++++++++++ .../index/styles/automatic-login-rejected.css | 30 +++++++++++++++++++ guacamole/src/main/frontend/src/index.html | 14 +++++++++ 3 files changed, 69 insertions(+) create mode 100644 guacamole/src/main/frontend/src/app/index/styles/automatic-login-rejected.css diff --git a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js index 52c9d5853..40cb9540d 100644 --- a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js +++ b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js @@ -35,6 +35,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', const SESSION_VALIDITY_RECHECK_INTERVAL = 15000; // Required types + const Error = $injector.get('Error'); const ManagedClientState = $injector.get('ManagedClientState'); // Required services @@ -110,6 +111,11 @@ angular.module('index').controller('indexController', ['$scope', '$injector', */ var ApplicationState = { + /** + * A non-interactive authentication attempt failed. + */ + AUTOMATIC_LOGIN_REJECTED : 'automaticLoginRejected', + /** * The application has fully loaded but is awaiting credentials from * the user before proceeding. @@ -323,6 +329,25 @@ angular.module('index').controller('indexController', ['$scope', '$injector', }); + // Alert user to authentication errors that occur in the absence of an + // interactive login form + $scope.$on('guacLoginFailed', function loginFailed(event, parameters, error) { + + // All errors related to an interactive login form are handled elsewhere + if ($scope.applicationState === ApplicationState.AWAITING_CREDENTIALS + || error.type === Error.Type.INSUFFICIENT_CREDENTIALS + || error.type === Error.Type.INVALID_CREDENTIALS) + return; + + $scope.applicationState = ApplicationState.AUTOMATIC_LOGIN_REJECTED; + $scope.page.title = 'APP.NAME'; + $scope.page.bodyClassName = ''; + + $scope.reAuthenticating = false; + $scope.fatalError = error; + + }); + // Replace absolutely all content with an error message if the page itself // cannot be displayed due to an error $scope.$on('guacFatalPageError', function fatalPageError(error) { diff --git a/guacamole/src/main/frontend/src/app/index/styles/automatic-login-rejected.css b/guacamole/src/main/frontend/src/app/index/styles/automatic-login-rejected.css new file mode 100644 index 000000000..d034f81f1 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/index/styles/automatic-login-rejected.css @@ -0,0 +1,30 @@ +/* + * 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. + */ + +.automatic-login-rejected-modal guac-modal { + background: white; + z-index: 20; +} + +.automatic-login-rejected-modal .notification { + display: inline-block; + max-width: 5in; + padding: 1em; + width: 100%; +} diff --git a/guacamole/src/main/frontend/src/index.html b/guacamole/src/main/frontend/src/index.html index 7f007eb55..fd6337872 100644 --- a/guacamole/src/main/frontend/src/index.html +++ b/guacamole/src/main/frontend/src/index.html @@ -64,6 +64,20 @@
    + + + Date: Thu, 23 Mar 2023 00:16:32 -0700 Subject: [PATCH 135/289] GUACAMOLE-1756: Clean up assignment of application state. --- .../app/index/controllers/indexController.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js index 40cb9540d..2f472a6e2 100644 --- a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js +++ b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js @@ -284,6 +284,22 @@ angular.module('index').controller('indexController', ['$scope', '$injector', }, true); + /** + * Sets the current overall state of the client side of the + * application to the given value. Possible values are defined by + * {@link ApplicationState}. The title and class associated with the + * current page are automatically reset to the standard values applicable + * to the application as a whole (rather than any specific page). + * + * @param {!string} state + * The state to assign, as defined by {@link ApplicationState}. + */ + const setApplicationState = function setApplicationState(state) { + $scope.applicationState = state; + $scope.page.title = 'APP.NAME'; + $scope.page.bodyClassName = ''; + }; + /** * Navigates the user back to the root of the application (or reloads the * current route and controller if the user is already there), effectively @@ -306,9 +322,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Display login screen if a whole new set of credentials is needed $scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) { - $scope.applicationState = ApplicationState.AWAITING_CREDENTIALS; - $scope.page.title = 'APP.NAME'; - $scope.page.bodyClassName = ''; + setApplicationState(ApplicationState.AWAITING_CREDENTIALS); $scope.loginHelpText = null; $scope.acceptedCredentials = {}; @@ -319,9 +333,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Prompt for remaining credentials if provided credentials were not enough $scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) { - $scope.applicationState = ApplicationState.AWAITING_CREDENTIALS; - $scope.page.title = 'APP.NAME'; - $scope.page.bodyClassName = ''; + setApplicationState(ApplicationState.AWAITING_CREDENTIALS); $scope.loginHelpText = error.translatableMessage; $scope.acceptedCredentials = parameters; @@ -339,10 +351,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', || error.type === Error.Type.INVALID_CREDENTIALS) return; - $scope.applicationState = ApplicationState.AUTOMATIC_LOGIN_REJECTED; - $scope.page.title = 'APP.NAME'; - $scope.page.bodyClassName = ''; - + setApplicationState(ApplicationState.AUTOMATIC_LOGIN_REJECTED); $scope.reAuthenticating = false; $scope.fatalError = error; @@ -351,13 +360,8 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Replace absolutely all content with an error message if the page itself // cannot be displayed due to an error $scope.$on('guacFatalPageError', function fatalPageError(error) { - - $scope.applicationState = ApplicationState.FATAL_ERROR; - $scope.page.title = 'APP.NAME'; - $scope.page.bodyClassName = ''; - + setApplicationState(ApplicationState.FATAL_ERROR); $scope.fatalError = error; - }); // Replace the overall user interface with an informational message if the From e6449d2c5756343ef8146e67ccfeef43ad9d268b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 23 Mar 2023 12:30:17 -0700 Subject: [PATCH 136/289] GUACAMOLE-1757: Ensure SSO provider list is added to login UI only once. --- .../src/main/resources/html/sso-providers.html | 2 +- .../src/main/resources/styles/sso-providers.css | 6 ------ .../src/main/resources/html/sso-provider-cas.html | 2 +- .../src/main/resources/html/sso-provider-openid.html | 2 +- .../src/main/resources/html/sso-provider-saml.html | 2 +- .../src/main/resources/html/sso-provider-ssl.html | 2 +- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/html/sso-providers.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/html/sso-providers.html index d089ea292..309d1bb7b 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/html/sso-providers.html +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/html/sso-providers.html @@ -1,4 +1,4 @@ - +
    {{ 'LOGIN.SECTION_HEADER_SSO_OPTIONS' | translate }}
      diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/styles/sso-providers.css b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/styles/sso-providers.css index 0a97446ec..5f8b49ae6 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/styles/sso-providers.css +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/styles/sso-providers.css @@ -24,12 +24,6 @@ bottom: 0; left: 0; - display: none; - -} - -.login-ui .sso-providers:last-child { - display: block; } .sso-providers ul { diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/html/sso-provider-cas.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/html/sso-provider-cas.html index 348da10fa..6fcf14e59 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/html/sso-provider-cas.html +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-cas/src/main/resources/html/sso-provider-cas.html @@ -1,4 +1,4 @@ - +
    • {{ 'LOGIN.NAME_IDP_CAS' | translate }}
    • diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/html/sso-provider-openid.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/html/sso-provider-openid.html index 0da260f96..5325e63ea 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/html/sso-provider-openid.html +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-openid/src/main/resources/html/sso-provider-openid.html @@ -1,4 +1,4 @@ - +
    • {{ 'LOGIN.NAME_IDP_OPENID' | translate }}
    • diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/html/sso-provider-saml.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/html/sso-provider-saml.html index 93a770434..734f7f41a 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/html/sso-provider-saml.html +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-saml/src/main/resources/html/sso-provider-saml.html @@ -1,4 +1,4 @@ - +
    • {{ 'LOGIN.NAME_IDP_SAML' | translate }}
    • diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html index 8798eed2d..d9a3db85d 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/resources/html/sso-provider-ssl.html @@ -1,4 +1,4 @@ - +
    • {{ 'LOGIN.NAME_IDP_SSL' | translate }}
    • From 0db69594cd5c4d3a3a2ffc637075676b916ad702 Mon Sep 17 00:00:00 2001 From: Victor Cleaner Date: Fri, 24 Mar 2023 18:52:51 +0100 Subject: [PATCH 137/289] GUACAMOLE-1758: Updated czech translation GUACAMOLE-1758: Updated czech translation --- .../main/frontend/src/translations/cs.json | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/guacamole/src/main/frontend/src/translations/cs.json b/guacamole/src/main/frontend/src/translations/cs.json index c02e13c69..67df484e7 100644 --- a/guacamole/src/main/frontend/src/translations/cs.json +++ b/guacamole/src/main/frontend/src/translations/cs.json @@ -58,6 +58,7 @@ "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CLEAR_CLIENT_MESSAGES" : "Vyčistit", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Vyčistit", "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_DISCONNECT" : "Odpojit", @@ -123,8 +124,9 @@ "HELP_MOUSE_MODE_RELATIVE" : "Přetažením myši posuňte ukazatel myši a klepněte na tlačítko. Kliknutí nastane v místě ukazatele.", "HELP_SHARE_LINK" : "Aktuální připojení je sdíleno a může k němu přistupovat kdokoli s následujícím {LINKS, plural, one{odkazem} other{odkazy}}:", - "INFO_CONNECTION_SHARED" : "Toto připojení je nyní sdíleno.", - "INFO_NO_FILE_TRANSFERS" : "Žádné přenosy souborů.", + "INFO_ANONYMOUS_USER_COUNT" : "Anonymní{COUNT, plural, one{} other{ (#)}}", + "INFO_CONNECTION_SHARED" : "Toto připojení je nyní sdíleno.", + "INFO_NO_FILE_TRANSFERS" : "Žádné přenosy souborů.", "NAME_INPUT_METHOD_NONE" : "Žádné", "NAME_INPUT_METHOD_OSK" : "Na obrazovce, klávesnice", @@ -136,21 +138,28 @@ "NAME_MOUSE_MODE_ABSOLUTE" : "Dotyková obrazovka", "NAME_MOUSE_MODE_RELATIVE" : "Touchpad", - "SECTION_HEADER_CLIPBOARD" : "Schránka", - "SECTION_HEADER_DEVICES" : "Zařízení", - "SECTION_HEADER_DISPLAY" : "Zobrazení", - "SECTION_HEADER_FILE_TRANSFERS" : "Přenos souborů", - "SECTION_HEADER_INPUT_METHOD" : "Metoda vstupu", - "SECTION_HEADER_MOUSE_MODE" : "Mód emulace myši", + "SECTION_HEADER_CLIENT_MESSAGES" : "Zprávy", + "SECTION_HEADER_CLIPBOARD" : "Schránka", + "SECTION_HEADER_DEVICES" : "Zařízení", + "SECTION_HEADER_DISPLAY" : "Zobrazení", + "SECTION_HEADER_FILE_TRANSFERS" : "Přenos souborů", + "SECTION_HEADER_INPUT_METHOD" : "Metoda vstupu", + + "SECTION_HEADER_MOUSE_MODE" : "Mód emulace myši", + "TEXT_ANONYMOUS_USER_JOINED" : "K připojení se připojil anonymní uživatel.", + "TEXT_ANONYMOUS_USER_LEFT" : "Anonymní uživatel opustil připojení.", "TEXT_ZOOM_AUTO_FIT" : "Automaticky přizpůsobit prohlížeč oknu", "TEXT_CLIENT_STATUS_IDLE" : "Nečinný", "TEXT_CLIENT_STATUS_CONNECTING" : "Připojuji ke Guacamole...", "TEXT_CLIENT_STATUS_DISCONNECTED" : "Byl jste odpojen.", "TEXT_CLIENT_STATUS_UNSTABLE" : "Síťové spojení ke Guacamole serveru se zdá nestabilní.", "TEXT_CLIENT_STATUS_WAITING" : "Připojen ke Guacamole. Čekání na odpověď...", + "TEXT_USER_JOINED" : "{USERNAME} se připojil ke spojení.", + "TEXT_USER_LEFT" : "{USERNAME} opustil spojení.", "TEXT_RECONNECT_COUNTDOWN" : "Znovu připojuji {REMAINING} {REMAINING, plural, one{sekundu} other{sekund}}...", "TEXT_FILE_TRANSFER_PROGRESS" : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}", + "TEXT_CLIPBOARD_AWAITING_FOCUS" : "Kliknutím zobrazíte obsah schránky...", "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.json" @@ -914,6 +923,7 @@ "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_SAVE" : "@:APP.ACTION_SAVE", "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", @@ -940,6 +950,7 @@ "HELP_UPDATE_PASSWORD" : "Pokud chcete změnit heslo, zadejte své aktuální heslo a níže požadované nové heslo a klikněte na tlačítko „Aktualizovat heslo“. Změna se projeví okamžitě.", "INFO_PASSWORD_CHANGED" : "Heslo bylo změněno.", + "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "Uživatelská nastavení uložena.", "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", From 08e5938493e50abdfa54bbbe1c6a944d96db6cf8 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 28 Mar 2023 13:09:05 -0700 Subject: [PATCH 138/289] GUACAMOLE-839: Redirect user to proper URI for SSL/TLS client auth (rather than just refuse). --- .../src/main/resources/translations/en.json | 1 + .../ssl/AuthenticationProviderService.java | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json index 902b7aff1..085414b94 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-base/src/main/resources/translations/en.json @@ -21,6 +21,7 @@ "FIELD_HEADER_STATE" : "", "FIELD_HEADER_TICKET" : "", "INFO_IDP_REDIRECT_PENDING" : "Please wait, redirecting to identity provider...", + "INFO_REDIRECT_PENDING" : "Please wait while you are redirected...", "NAME_IDP_CAS" : "CAS", "NAME_IDP_OPENID" : "OpenID", "NAME_IDP_SAML" : "SAML", diff --git a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java index bc311de47..807df0cae 100644 --- a/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java +++ b/extensions/guacamole-auth-sso/modules/guacamole-auth-sso-ssl/src/main/java/org/apache/guacamole/auth/ssl/AuthenticationProviderService.java @@ -23,15 +23,20 @@ import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.net.URI; +import java.util.Arrays; import java.util.Collections; import javax.servlet.http.HttpServletRequest; -import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.auth.ssl.conf.ConfigurationService; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.auth.sso.SSOAuthenticationProviderService; import org.apache.guacamole.auth.sso.user.SSOAuthenticatedUser; +import org.apache.guacamole.form.Field; +import org.apache.guacamole.form.RedirectField; +import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.credentials.CredentialsInfo; +import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; /** * Service that authenticates Guacamole users using SSL/TLS authentication @@ -150,11 +155,15 @@ public class AuthenticationProviderService implements SSOAuthenticationProviderS if (confService.isPrimaryHostname(host)) return processIdentity(credentials, request); - // All other requests are not allowed - refuse to authenticate - throw new GuacamoleClientException("Direct authentication against " - + "this endpoint is not valid without first requesting to " - + "authenticate at the primary URL of this Guacamole " - + "instance."); + // All other requests are not allowed - redirect to proper hostname + throw new GuacamoleInvalidCredentialsException("Authentication is " + + "only allowed against the primary URL of this Guacamole " + + "instance.", + new CredentialsInfo(Arrays.asList(new Field[] { + new RedirectField("primaryURI", confService.getPrimaryURI(), + new TranslatableMessage("LOGIN.INFO_REDIRECT_PENDING")) + })) + ); } From 45955b67e078e645645b02ca8c451b77881ff59c Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 21 Mar 2023 18:00:35 +0000 Subject: [PATCH 139/289] GUACAMOLE-1754: Run docker build for PR CI. --- .github/workflows/pr-build.yml | 29 +++++++++++++++++++++++++++++ .ratignore | 1 + 2 files changed, 30 insertions(+) create mode 100644 .github/workflows/pr-build.yml diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 000000000..a7733ca50 --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,29 @@ +name: Pull request CI build + +# Run build for all pull requests +on: + pull_request: + +# Limit to only one build for a given PR source branch at a time, +# cancelling any in-progress builds +concurrency: + group: guacamole-client-pr-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + + docker_build: + name: Run docker build + runs-on: ubuntu-latest + steps: + + - name: Check out code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Build Docker container + shell: sh + run: | + docker build --pull --no-cache --force-rm . diff --git a/.ratignore b/.ratignore index c3f1957cd..d46e96082 100644 --- a/.ratignore +++ b/.ratignore @@ -1,3 +1,4 @@ +.github/** CONTRIBUTING doc/licenses/*/**/* doc/**/html/*.html From f079e87f27c4ea6c9c2f8cf52ba36541f5fbfff6 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Mon, 3 Apr 2023 19:46:48 -0400 Subject: [PATCH 140/289] GUACAMOLE-1642: Add support for specifying the VNC encodings parameter. --- .../main/resources/org/apache/guacamole/protocols/vnc.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json index a14b3dd26..525e1958e 100644 --- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json +++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json @@ -57,6 +57,10 @@ "name" : "force-lossless", "type" : "BOOLEAN", "options" : [ "true" ] + }, + { + "name" : "encodings", + "type" : "TEXT" } ] }, From de3a1f5ec6c7809f83b424d3f78c7420d608cb34 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 4 Apr 2023 09:10:13 -0700 Subject: [PATCH 141/289] GUACAMOLE-839: Omit properties added by AngularJS core from auth request parameters. --- .../frontend/src/app/auth/service/authenticationService.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js index 88143a740..af5a9ab6c 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js @@ -196,6 +196,13 @@ angular.module('auth').factory('authenticationService', ['$injector', // Attempt authentication after auth parameters are available ... return parameters.then(function requestParametersReady(requestParams) { + // Strip any properties that are from AngularJS core, such as the + // '$$state' property added by $q. Properties added by AngularJS + // core will have a '$' prefix. The '$$state' property is + // particularly problematic, as it is self-referential and explodes + // the stack when fed to $.param(). + requestParams = _.omitBy(requestParams, (value, key) => key.startsWith('$')); + return requestService({ method: 'POST', url: 'api/tokens', From 97e99d6fe37a3dcaa4725c7dcf25a3d72768a107 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 18 Jan 2023 21:59:01 +0000 Subject: [PATCH 142/289] GUACAMOLE-926: Create directory infrastructure for batch creation. --- .../net/auth/AtomicDirectoryOperation.java | 57 ++++++++ .../apache/guacamole/net/auth/Directory.java | 85 ++++++++++- .../DirectoryOperationException.java | 12 ++ .../rest/directory/DirectoryResource.java | 134 +++++++++++++++--- 4 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java new file mode 100644 index 000000000..1d5e196eb --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/AtomicDirectoryOperation.java @@ -0,0 +1,57 @@ +/* + * 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.auth; + +import org.apache.guacamole.GuacamoleException; + +/** + * An operation that should be attempted atomically when passed to + * {@link Directory#tryAtomically()}, if atomic operations are supported by + * the Directory. + */ +public interface AtomicDirectoryOperation { + + /** + * Attempt the operation atomically. If the Directory does not support + * atomic operations, the atomic flag will be set to false. If the atomic + * flag is set to true, the provided directory is guaranteed to perform + * the operations within this function atomically. Atomicity of the + * provided directory outside this function, or of the directory invoking + * this function are not guaranteed. + * + * NOTE: If atomicity is required for this operation, a GuacamoleException + * may be thrown by this function before any changes are made, ensuring the + * operation will only ever be performed atomically. + * + * @param atomic + * True if the provided directory is guaranteed to peform the operation + * atomically within the context of this function. + * + * @param directory + * A directory that will perform the operation atomically if the atomic + * flag is set to true. If the flag is false, the directory may still + * be used, though atomicity is not guaranteed. + * + * @throws GuacamoleException + * If an issue occurs during the operation. + */ + void executeOperation(boolean atomic, Directory directory) + throws GuacamoleException; +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java index ba1e1522d..ac76fae9d 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java @@ -20,6 +20,7 @@ package org.apache.guacamole.net.auth; import java.util.Collection; +import java.util.Iterator; import java.util.Set; import org.apache.guacamole.GuacamoleException; @@ -198,6 +199,29 @@ public interface Directory { void add(ObjectType object) throws GuacamoleException; + /** + * Adds the given objects to the overall set. If new identifiers are + * created for any of the the added objects, the identifiers will be + * automatically assigned via setIdentifier(). + * + * @param objects + * The objects to add. + * + * @throws GuacamoleException + * If an error occurs while adding any of the objects, or if adding + * the objects is not allowed. + */ + default void add(Collection objects) + throws GuacamoleException { + + // Add each object individually by default + Iterator iterator = objects.iterator(); + while (iterator.hasNext()) { + add(iterator.next()); + } + + } + /** * Updates the stored object with the data contained in the given object. * @@ -209,14 +233,73 @@ public interface Directory { void update(ObjectType object) throws GuacamoleException; + /** + * Updates the stored objects with the data contained in the given objects. + * + * @param objects The objects which will supply the data for the update. + * + * @throws GuacamoleException If an error occurs while updating the objects, + * or if updating an object is not allowed. + */ + default void update(Collection objects) + throws GuacamoleException { + + // Update each object individually by default + Iterator iterator = objects.iterator(); + while (iterator.hasNext()) { + update(iterator.next()); + } + + } + /** * Removes the object with the given identifier from the overall set. * * @param identifier The identifier of the object to remove. * * @throws GuacamoleException If an error occurs while removing the object, - * or if removing object is not allowed. + * or if removing the object is not allowed. */ void remove(String identifier) throws GuacamoleException; + /** + * Removes all object with any of the given identifier from the overall set. + * + * @param identifiers The identifiers of the objects to remove. + * + * @throws GuacamoleException If an error occurs while removing an object, + * or if removing an object is not allowed. + */ + default void remove(Collection identifiers) + throws GuacamoleException { + + // Remove each object individually by default + Iterator iterator = identifiers.iterator(); + while (iterator.hasNext()) { + remove(iterator.next()); + } + + } + + /** + * Attempt to perform the provided operation atomically if possible. If the + * operation can be performed atomically, the atomic flag will be set to + * true, and the directory passed to the provided operation callback will + * peform directory operations atomically within the operation callback. + * + * @param operation + * The directory operation that should be performed atomically. + * + * @throws GuacamoleException + * If an error occurs during execution of the provided operation. + */ + default void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + + // By default, perform the operation non-atomically. If atomic operation + // is supported by an implementation, it must be implemented there. + operation.executeOperation(false, this); + + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java new file mode 100644 index 000000000..3a88d4270 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java @@ -0,0 +1,12 @@ +package org.apache.guacamole.rest.directory; + +import org.apache.guacamole.GuacamoleException; + +public class DirectoryOperationException extends GuacamoleException { + + public DirectoryOperationException(String message) { + super(message); + } + + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 50d428528..8aa464bd0 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -19,8 +19,10 @@ package org.apache.guacamole.rest.directory; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import javax.inject.Inject; @@ -37,6 +39,7 @@ import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; import org.apache.guacamole.GuacamoleUnsupportedException; +import org.apache.guacamole.net.auth.AtomicDirectoryOperation; import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.Directory; @@ -341,6 +344,17 @@ public abstract class DirectoryResource> patches) + public void patchObjects(List> patches) throws GuacamoleException { - // Apply each operation specified within the patch - for (APIPatch patch : patches) { + // Objects will be add, updated, and removed atomically + Collection objectsToAdd = new ArrayList<>(); + Collection objectsToUpdate = new ArrayList<>(); + Collection identifiersToRemove = new ArrayList<>(); - // Only remove is supported - if (patch.getOp() != APIPatch.Operation.remove) - throw new GuacamoleUnsupportedException("Only the \"remove\" " - + "operation is supported."); + // Apply each operation specified within the patch + for (APIPatch patch : patches) { // Retrieve and validate path String path = patch.getPath(); if (!path.startsWith("/")) throw new GuacamoleClientException("Patch paths must start with \"/\"."); - // Remove specified object - String identifier = path.substring(1); - try { - directory.remove(identifier); - fireDirectorySuccessEvent(DirectoryEvent.Operation.REMOVE, identifier, null); + // Append each provided object to the list, to be added atomically + if(patch.getOp() == APIPatch.Operation.add) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + // Add to the list of objects to create + objectsToAdd.add(internal); } - catch (GuacamoleException | RuntimeException | Error e) { - fireDirectoryFailureEvent(DirectoryEvent.Operation.REMOVE, identifier, null, e); - throw e; + + // Append each provided object to the list, to be updated atomically + else if (patch.getOp() == APIPatch.Operation.replace) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + // Add to the list of objects to update + objectsToUpdate.add(internal); } + // Append each identifier to the list, to be removed atomically + else if (patch.getOp() == APIPatch.Operation.remove) { + + String identifier = path.substring(1); + identifiersToRemove.add(identifier); + + } + + } + + // Perform all requested operations atomically + directory.tryAtomically(new AtomicDirectoryOperation() { + + @Override + public void executeOperation(boolean atomic, Directory directory) + throws GuacamoleException { + + // If the underlying directory implentation does not support + // atomic operations, abort the patch operation. This REST + // endpoint requires that operations be performed atomically. + if (!atomic) + throw new GuacamoleUnsupportedException( + "Atomic operations are not supported. " + + "The patch cannot be executed."); + + // First, create every object from the patch + directory.add(objectsToAdd); + + // Next, update every object from the patch + directory.update(objectsToUpdate); + + // Finally, remove every object from the patch + directory.remove(identifiersToRemove); + + } + + }); + + // Fire directory success events for each created object + Iterator addedIterator = objectsToAdd.iterator(); + while (addedIterator.hasNext()) { + + InternalType internal = addedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each updated object + Iterator updatedIterator = objectsToUpdate.iterator(); + while (updatedIterator.hasNext()) { + + InternalType internal = updatedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each removed object + Iterator removedIterator = identifiersToRemove.iterator(); + while (removedIterator.hasNext()) { + + String identifier = removedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, identifier, null); + } } @@ -453,8 +544,7 @@ public abstract class DirectoryResource Date: Wed, 18 Jan 2023 23:57:02 +0000 Subject: [PATCH 143/289] GUACAMOLE-926: Set JDBC to batch mode and just do operations one at a time. --- .../JDBCAuthenticationProviderModule.java | 6 + .../ActiveConnectionDirectory.java | 6 +- .../auth/jdbc/base/JDBCDirectory.java | 44 ++++ .../jdbc/connection/ConnectionDirectory.java | 6 +- .../ConnectionGroupDirectory.java | 6 +- .../SharingProfileDirectory.java | 6 +- .../auth/jdbc/user/UserDirectory.java | 6 +- .../jdbc/usergroup/UserGroupDirectory.java | 6 +- .../apache/guacamole/net/auth/Directory.java | 62 ------ .../DirectoryOperationException.java | 12 -- .../rest/directory/DirectoryResource.java | 204 +++++++++++------- 11 files changed, 188 insertions(+), 176 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.java delete mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java index 5ae0ea53f..7b735bdf4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java @@ -45,6 +45,7 @@ import org.apache.guacamole.auth.jdbc.security.SaltService; import org.apache.guacamole.auth.jdbc.security.SecureRandomSaltService; import org.apache.guacamole.auth.jdbc.permission.SystemPermissionService; import org.apache.guacamole.auth.jdbc.user.UserService; +import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.apache.guacamole.auth.jdbc.permission.ConnectionGroupPermissionMapper; import org.apache.guacamole.auth.jdbc.permission.ConnectionGroupPermissionService; @@ -126,6 +127,11 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule { // Transaction factory bindTransactionFactoryType(JdbcTransactionFactory.class); + // Set the JDBC Auth provider to use batch execution when possible + bindConfigurationSetting(configuration -> { + configuration.setDefaultExecutorType(ExecutorType.BATCH); + }); + // Add MyBatis mappers addMapperClass(ConnectionMapper.class); addMapperClass(ConnectionGroupMapper.class); diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java index b0d6324aa..28510b738 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionDirectory.java @@ -25,16 +25,14 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.ActiveConnection; -import org.apache.guacamole.net.auth.Directory; /** * Implementation of a Directory which contains all currently-active * connections. */ -public class ActiveConnectionDirectory extends RestrictedObject - implements Directory { +public class ActiveConnectionDirectory extends JDBCDirectory { /** * Service for retrieving and manipulating active connections. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.java new file mode 100644 index 000000000..fcba2f678 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/JDBCDirectory.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.jdbc.base; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.AtomicDirectoryOperation; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.Identifiable; +import org.mybatis.guice.transactional.Transactional; + +/** + * An implementation of Directory that uses database transactions to guarantee + * atomicity for any operations supplied to tryAtomically(). + */ +public abstract class JDBCDirectory + extends RestrictedObject implements Directory { + + @Transactional + public void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + + // Execute the operation atomically - the @Transactional annotation + // specifies that the entire operation will be performed in a transaction + operation.executeOperation(true, this); + + } +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 52a127df4..3e364f509 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -25,17 +25,15 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.Connection; -import org.apache.guacamole.net.auth.Directory; import org.mybatis.guice.transactional.Transactional; /** * Implementation of the Connection Directory which is driven by an underlying, * arbitrary database. */ -public class ConnectionDirectory extends RestrictedObject - implements Directory { +public class ConnectionDirectory extends JDBCDirectory { /** * Service for managing connection objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java index 9f3930597..2e21dc21d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupDirectory.java @@ -25,17 +25,15 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.ConnectionGroup; -import org.apache.guacamole.net.auth.Directory; import org.mybatis.guice.transactional.Transactional; /** * Implementation of the ConnectionGroup Directory which is driven by an * underlying, arbitrary database. */ -public class ConnectionGroupDirectory extends RestrictedObject - implements Directory { +public class ConnectionGroupDirectory extends JDBCDirectory { /** * Service for managing connection group objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java index 632557052..4035ff03f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharingprofile/SharingProfileDirectory.java @@ -24,8 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; -import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.SharingProfile; import org.mybatis.guice.transactional.Transactional; @@ -33,8 +32,7 @@ import org.mybatis.guice.transactional.Transactional; * Implementation of the SharingProfile Directory which is driven by an * underlying, arbitrary database. */ -public class SharingProfileDirectory extends RestrictedObject - implements Directory { +public class SharingProfileDirectory extends JDBCDirectory { /** * Service for managing sharing profile objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java index dffd8e2ec..72d5a4f30 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserDirectory.java @@ -25,8 +25,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; -import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.User; import org.mybatis.guice.transactional.Transactional; @@ -34,8 +33,7 @@ import org.mybatis.guice.transactional.Transactional; * Implementation of the User Directory which is driven by an underlying, * arbitrary database. */ -public class UserDirectory extends RestrictedObject - implements Directory { +public class UserDirectory extends JDBCDirectory { /** * Service for managing user objects. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java index 911b8521f..c6bb89572 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/usergroup/UserGroupDirectory.java @@ -24,8 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.jdbc.base.RestrictedObject; -import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.auth.jdbc.base.JDBCDirectory; import org.apache.guacamole.net.auth.UserGroup; import org.mybatis.guice.transactional.Transactional; @@ -33,8 +32,7 @@ import org.mybatis.guice.transactional.Transactional; * Implementation of the UserGroup Directory which is driven by an underlying, * arbitrary database. */ -public class UserGroupDirectory extends RestrictedObject - implements Directory { +public class UserGroupDirectory extends JDBCDirectory { /** * Service for managing user group objects. diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java index ac76fae9d..bc5c52afa 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Directory.java @@ -20,7 +20,6 @@ package org.apache.guacamole.net.auth; import java.util.Collection; -import java.util.Iterator; import java.util.Set; import org.apache.guacamole.GuacamoleException; @@ -199,29 +198,6 @@ public interface Directory { void add(ObjectType object) throws GuacamoleException; - /** - * Adds the given objects to the overall set. If new identifiers are - * created for any of the the added objects, the identifiers will be - * automatically assigned via setIdentifier(). - * - * @param objects - * The objects to add. - * - * @throws GuacamoleException - * If an error occurs while adding any of the objects, or if adding - * the objects is not allowed. - */ - default void add(Collection objects) - throws GuacamoleException { - - // Add each object individually by default - Iterator iterator = objects.iterator(); - while (iterator.hasNext()) { - add(iterator.next()); - } - - } - /** * Updates the stored object with the data contained in the given object. * @@ -233,25 +209,6 @@ public interface Directory { void update(ObjectType object) throws GuacamoleException; - /** - * Updates the stored objects with the data contained in the given objects. - * - * @param objects The objects which will supply the data for the update. - * - * @throws GuacamoleException If an error occurs while updating the objects, - * or if updating an object is not allowed. - */ - default void update(Collection objects) - throws GuacamoleException { - - // Update each object individually by default - Iterator iterator = objects.iterator(); - while (iterator.hasNext()) { - update(iterator.next()); - } - - } - /** * Removes the object with the given identifier from the overall set. * @@ -262,25 +219,6 @@ public interface Directory { */ void remove(String identifier) throws GuacamoleException; - /** - * Removes all object with any of the given identifier from the overall set. - * - * @param identifiers The identifiers of the objects to remove. - * - * @throws GuacamoleException If an error occurs while removing an object, - * or if removing an object is not allowed. - */ - default void remove(Collection identifiers) - throws GuacamoleException { - - // Remove each object individually by default - Iterator iterator = identifiers.iterator(); - while (iterator.hasNext()) { - remove(iterator.next()); - } - - } - /** * Attempt to perform the provided operation atomically if possible. If the * operation can be performed atomically, the atomic flag will be set to diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java deleted file mode 100644 index 3a88d4270..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryOperationException.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.apache.guacamole.rest.directory; - -import org.apache.guacamole.GuacamoleException; - -public class DirectoryOperationException extends GuacamoleException { - - public DirectoryOperationException(String message) { - super(message); - } - - -} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 8aa464bd0..412068a96 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -417,49 +417,6 @@ public abstract class DirectoryResource> patches) throws GuacamoleException { - // Objects will be add, updated, and removed atomically - Collection objectsToAdd = new ArrayList<>(); - Collection objectsToUpdate = new ArrayList<>(); - Collection identifiersToRemove = new ArrayList<>(); - - // Apply each operation specified within the patch - for (APIPatch patch : patches) { - - // Retrieve and validate path - String path = patch.getPath(); - if (!path.startsWith("/")) - throw new GuacamoleClientException("Patch paths must start with \"/\"."); - - // Append each provided object to the list, to be added atomically - if(patch.getOp() == APIPatch.Operation.add) { - - // Filter/sanitize object contents - InternalType internal = filterAndTranslate(patch.getValue()); - - // Add to the list of objects to create - objectsToAdd.add(internal); - } - - // Append each provided object to the list, to be updated atomically - else if (patch.getOp() == APIPatch.Operation.replace) { - - // Filter/sanitize object contents - InternalType internal = filterAndTranslate(patch.getValue()); - - // Add to the list of objects to update - objectsToUpdate.add(internal); - } - - // Append each identifier to the list, to be removed atomically - else if (patch.getOp() == APIPatch.Operation.remove) { - - String identifier = path.substring(1); - identifiersToRemove.add(identifier); - - } - - } - // Perform all requested operations atomically directory.tryAtomically(new AtomicDirectoryOperation() { @@ -475,48 +432,139 @@ public abstract class DirectoryResource addedObjects = new ArrayList<>(); + Collection updatedObjects = new ArrayList<>(); + Collection removedIdentifiers = new ArrayList<>(); - // Finally, remove every object from the patch - directory.remove(identifiersToRemove); + // True if any operation in the patch failed. Any failure will + // fail the request, though won't result in immediate stoppage + // since more errors may yet be uncovered. + boolean failed = false; + + // Apply each operation specified within the patch + for (APIPatch patch : patches) { + + // Retrieve and validate path + String path = patch.getPath(); + if (!path.startsWith("/")) + throw new GuacamoleClientException("Patch paths must start with \"/\"."); + + if(patch.getOp() == APIPatch.Operation.add) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + try { + + // Attempt to add the new object + directory.add(internal); + + // Add the object to the list if addition was successful + addedObjects.add(internal); + + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent( + DirectoryEvent.Operation.ADD, + internal.getIdentifier(), internal, e); + + // TODO: Save the error for later inclusion in a big JSON error response + failed = true; + } + + } + + else if (patch.getOp() == APIPatch.Operation.replace) { + + // Filter/sanitize object contents + InternalType internal = filterAndTranslate(patch.getValue()); + + try { + + // Attempt to update the object + directory.update(internal); + + // Add the object to the list if the update was successful + updatedObjects.add(internal); + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent( + DirectoryEvent.Operation.UPDATE, + internal.getIdentifier(), internal, e); + + // TODO: Save the error for later inclusion in a big JSON error response + failed = true; + } + } + + // Append each identifier to the list, to be removed atomically + else if (patch.getOp() == APIPatch.Operation.remove) { + + String identifier = path.substring(1); + + try { + + // Attempt to remove the object + directory.remove(identifier); + + // Add the object to the list if the removal was successful + removedIdentifiers.add(identifier); + } + catch (GuacamoleException | RuntimeException | Error e) { + fireDirectoryFailureEvent( + DirectoryEvent.Operation.UPDATE, identifier, null, e); + + // TODO: Save the error for later inclusion in a big JSON error response + failed = true; + } + } + + } + + // If any operation failed, fail now + if (failed) { + throw new GuacamoleClientException( + "oh noes the patch batch failed"); + } + + // Fire directory success events for each created object + Iterator addedIterator = addedObjects.iterator(); + while (addedIterator.hasNext()) { + + InternalType internal = addedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each updated object + Iterator updatedIterator = updatedObjects.iterator(); + while (updatedIterator.hasNext()) { + + InternalType internal = updatedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); + + } + + // Fire directory success events for each removed object + Iterator removedIterator = removedIdentifiers.iterator(); + while (removedIterator.hasNext()) { + + String identifier = removedIterator.next(); + fireDirectorySuccessEvent( + DirectoryEvent.Operation.UPDATE, identifier, null); + + } } }); - // Fire directory success events for each created object - Iterator addedIterator = objectsToAdd.iterator(); - while (addedIterator.hasNext()) { - - InternalType internal = addedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.ADD, internal.getIdentifier(), internal); - - } - - // Fire directory success events for each updated object - Iterator updatedIterator = objectsToUpdate.iterator(); - while (updatedIterator.hasNext()) { - - InternalType internal = updatedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, internal.getIdentifier(), internal); - - } - - // Fire directory success events for each removed object - Iterator removedIterator = identifiersToRemove.iterator(); - while (removedIterator.hasNext()) { - - String identifier = removedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, identifier, null); - - } + // TODO: JSON response with failures or success } From 7d1d5cdf1338f86043b54dd1e00012495923a238 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 19 Jan 2023 00:45:38 +0000 Subject: [PATCH 144/289] GUACAMOLE-926: Success / Error handling in REST API. --- .../src/main/java/org/apache/guacamole/rest/APIError.java | 2 ++ .../apache/guacamole/rest/directory/DirectoryResource.java | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java index 846e2930e..0d925d04f 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java @@ -202,6 +202,8 @@ public class APIError { this.translatableMessage = translatable.getTranslatableMessage(); } + // TODO: Handle patch exceptions, need a bunch of JSON saying which things failed + // Use generic translation string if message is not translated else this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY, diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 412068a96..9ca71dba0 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -564,7 +564,9 @@ public abstract class DirectoryResource Date: Mon, 23 Jan 2023 23:40:48 +0000 Subject: [PATCH 145/289] GUACAMOLE-926: Improve response plumbing through to user. --- .../jdbc/connection/ConnectionDirectory.java | 15 +- .../net/auth/DelegatingDirectory.java | 6 + .../org/apache/guacamole/rest/APIError.java | 27 +++- .../rest/directory/DirectoryResource.java | 142 +++++++++++++++--- .../identifier/RelatedObjectSetResource.java | 2 +- .../rest/{ => jsonpatch}/APIPatch.java | 6 +- .../rest/jsonpatch/APIPatchError.java | 71 +++++++++ .../jsonpatch/APIPatchFailureException.java | 66 ++++++++ .../rest/jsonpatch/APIPatchOutcome.java | 110 ++++++++++++++ .../rest/jsonpatch/APIPatchResponse.java | 56 +++++++ .../rest/jsonpatch/package-info.java | 24 +++ .../permission/PermissionSetResource.java | 2 +- 12 files changed, 497 insertions(+), 30 deletions(-) rename guacamole/src/main/java/org/apache/guacamole/rest/{ => jsonpatch}/APIPatch.java (93%) create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java create mode 100644 guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 3e364f509..775628889 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -68,8 +68,19 @@ public class ConnectionDirectory extends JDBCDirectory { @Override @Transactional public void update(Connection object) throws GuacamoleException { - ModeledConnection connection = (ModeledConnection) object; - connectionService.updateObject(getCurrentUser(), connection); + + // If the provided connection is already an internal type, update + // using the internal method + if (object instanceof ModeledConnection) { + ModeledConnection connection = (ModeledConnection) object; + connectionService.updateObject(getCurrentUser(), connection); + } + + // If the type is not already the expected internal type, use the + // external update method + else { + connectionService.updateExternalObject(getCurrentUser(), object); + } } @Override diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java index 0a9046c01..cc19847c1 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingDirectory.java @@ -90,4 +90,10 @@ public class DelegatingDirectory directory.remove(identifier); } + @Override + public void tryAtomically(AtomicDirectoryOperation operation) + throws GuacamoleException { + directory.tryAtomically(operation); + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java index 0d925d04f..63b55289b 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/APIError.java @@ -21,6 +21,8 @@ package org.apache.guacamole.rest; import java.util.Collection; import java.util.Collections; +import java.util.List; + import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleResourceNotFoundException; @@ -31,6 +33,8 @@ import org.apache.guacamole.language.TranslatableMessage; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; +import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException; +import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome; import org.apache.guacamole.tunnel.GuacamoleStreamException; /** @@ -71,6 +75,12 @@ public class APIError { */ private final Collection expected; + /** + * The outcome of each patch in the associated request, if this was a + * JSON Patch request. Otherwise null. + */ + private List patches = null; + /** * The type of error that occurred. */ @@ -202,13 +212,14 @@ public class APIError { this.translatableMessage = translatable.getTranslatableMessage(); } - // TODO: Handle patch exceptions, need a bunch of JSON saying which things failed - // Use generic translation string if message is not translated else this.translatableMessage = new TranslatableMessage(UNTRANSLATED_MESSAGE_KEY, Collections.singletonMap(UNTRANSLATED_MESSAGE_VARIABLE_NAME, this.message)); + if (exception instanceof APIPatchFailureException) + this.patches = ((APIPatchFailureException) exception).getPatches(); + } /** @@ -245,6 +256,18 @@ public class APIError { return expected; } + /** + * Return the outcome for every patch in the request, if the request was + * a JSON patch request. Otherwise, null. + * + * @return + * The outcome for every patch if responding to a JSON Patch request, + * otherwise null. + */ + public List getPatches() { + return patches; + } + /** * Returns a human-readable error message describing the error that * occurred. diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index 9ca71dba0..0b4e476bd 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -53,8 +53,12 @@ import org.apache.guacamole.net.auth.permission.SystemPermissionSet; import org.apache.guacamole.net.event.DirectoryEvent; import org.apache.guacamole.net.event.DirectoryFailureEvent; import org.apache.guacamole.net.event.DirectorySuccessEvent; -import org.apache.guacamole.rest.APIPatch; import org.apache.guacamole.rest.event.ListenerService; +import org.apache.guacamole.rest.jsonpatch.APIPatch; +import org.apache.guacamole.rest.jsonpatch.APIPatchError; +import org.apache.guacamole.rest.jsonpatch.APIPatchFailureException; +import org.apache.guacamole.rest.jsonpatch.APIPatchOutcome; +import org.apache.guacamole.rest.jsonpatch.APIPatchResponse; /** * A REST resource which abstracts the operations available on all Guacamole @@ -344,7 +348,20 @@ public abstract class DirectoryResource> patches) + public APIPatchResponse patchObjects(List> patches) throws GuacamoleException { + // An outcome for each patch included in the request. This list + // may include both success and failure responses, though the + // presense of any failure would indicated that the entire + // request has failed and no changes have been made. + List patchOutcomes = new ArrayList<>(); + // Perform all requested operations atomically directory.tryAtomically(new AtomicDirectoryOperation() { @@ -432,13 +460,16 @@ public abstract class DirectoryResource addedObjects = new ArrayList<>(); Collection updatedObjects = new ArrayList<>(); Collection removedIdentifiers = new ArrayList<>(); + // A list of all responses associated with the successful + // creation of new objects + List creationSuccesses = new ArrayList<>(); + // True if any operation in the patch failed. Any failure will // fail the request, though won't result in immediate stoppage // since more errors may yet be uncovered. @@ -465,14 +496,33 @@ public abstract class DirectoryResource response.clearIdentifier()); + + // Return an error response, including any failures that + // caused the failure of any patch in the request + throw new APIPatchFailureException( + "The provided patches failed to apply.", patchOutcomes); + } // Fire directory success events for each created object @@ -536,7 +634,8 @@ public abstract class DirectoryResource * The type of object being patched. diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java new file mode 100644 index 000000000..85d28a714 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchError.java @@ -0,0 +1,71 @@ +/* + * 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.rest.jsonpatch; + +import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation; + +/** + * A failure outcome associated with a particular patch within a JSON Patch + * request. This status indicates that a particular patch failed to apply, + * and includes the error describing the failure, along with the operation and + * path from the original patch, and the identifier of the object + * referenced by the original patch. + */ +public class APIPatchError extends APIPatchOutcome { + + /** + * The error associated with the submitted patch. + */ + private final String error; + + /** + * Create a failure status associated with a submitted patch from a JSON + * patch API request. + * + * @param op + * The operation requested by the failed patch. + * + * @param identifier + * The identifier of the object associated with the failed patch. If + * the patch failed to create a new object, this will be null. + * + * @param path + * The patch from the failed patch. + * + * @param error + * The error message associated with the failure that prevented the + * patch from applying. + */ + public APIPatchError( + Operation op, String identifier, String path, String error) { + super(op, identifier, path); + this.error = error; + } + + /** + * Return the error associated with the patch failure. + * + * @return + * The error associated with the patch failure. + */ + public String getError() { + return error; + } +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java new file mode 100644 index 000000000..fa40bffc6 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchFailureException.java @@ -0,0 +1,66 @@ +/* + * 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.rest.jsonpatch; + +import java.util.List; + +import org.apache.guacamole.GuacamoleClientException; + +/** + * An exception describing a failure to apply the patches from a JSON Patch + * request. A list of outcomes is included, one for each patch in the request. + */ +public class APIPatchFailureException extends GuacamoleClientException { + + /** + * A list of outcomes, each one corresponding to a patch in the request + * corresponding to this response. This may include a mix of successes and + * failures. Any failure will result in a failure of the entire request + * since JSON Patch requests are handled atomically. + */ + public final List patches; + + /** + * Create a new patch request failure with the provided list of outcomes + * for individual patches. + * + * @param message + * A human-readable message describing the overall request failure. + * + * @param patches + * A list of patch outcomes, one for each patch in the request + * associated with this response. + */ + public APIPatchFailureException( + String message, List patches) { + + super(message ); + this.patches = patches; + } + + /** + * Return the outcome for each patch in the request corresponding to this + * response. + */ + public List getPatches() { + return patches; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java new file mode 100644 index 000000000..245664cbc --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchOutcome.java @@ -0,0 +1,110 @@ +/* + * 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.rest.jsonpatch; + +import org.apache.guacamole.rest.jsonpatch.APIPatch.Operation; + +/** + * A successful outcome associated with a particular patch within a JSON Patch + * request. The outcome contains the operation requested by the original patch, + * the path from the original patch, and the identifier of the object corresponding + * to the value from the original patch. + * + * The purpose of this class is to present a relatively lightweight outcome for + * the user who submitted the Patch request. Rather than including the full + * contents of the value, only the identifier is included, allowing the user to + * determine the identifier of any newly-created objects as part of the request. + * + */ +public class APIPatchOutcome { + + /** + * The requested operation for the patch corresponding to this outcome. + */ + private final Operation op; + + /** + * The identifier for the value in patch corresponding to this outcome. + * If the value in the patch was null, this identifier should also be null. + */ + private String identifier; + + /** + * The path for the patch corresponding to this outcome. + */ + private final String path; + + /** + * Create an outcome associated with a submitted patch, as part of a JSON + * patch API request. + * + * @param op + * @param identifier + * @param path + */ + public APIPatchOutcome(Operation op, String identifier, String path) { + this.op = op; + this.identifier = identifier; + this.path = path; + } + + /** + * Clear the identifier associated with this patch outcome. This must + * be done when an identifier in a outcome refers to a temporary object + * that was rolled back during processing of a request. + */ + public void clearIdentifier() { + this.identifier = null; + } + + /** + * Returns the requested operation for the patch corresponding to this + * outcome. + * + * @return + * The requested operation for the patch corresponding to this outcome. + */ + public Operation getOp() { + return op; + } + + /** + * Returns the path for the patch corresponding to this outcome. + * + * @return + * The path for the patch corresponding to this outcome. + */ + public String getPath() { + return path; + } + + /** + * Returns the identifier for the value in patch corresponding to this + * outcome, or null if the value in the patch was null. + * + * @return + * The identifier for the value in patch corresponding to this + * outcome, or null if the value was null. + */ + public String getIdentifier() { + return identifier; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java new file mode 100644 index 000000000..21096a2bc --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/APIPatchResponse.java @@ -0,0 +1,56 @@ +/* + * 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.rest.jsonpatch; + +import java.util.List; + +/** + * A REST response describing the successful application of a JSON PATCH + * request to a directory. This consists of a list of outcomes, one for each + * patch within the request, in the same order. + */ +public class APIPatchResponse { + + /** + * A list of outcomes, each one corresponding to a patch in the request + * corresponding to this response. + */ + public final List patches; + + /** + * Create a new patch response with the provided list of outcomes for + * individual patches. + * + * @param patches + * A list of patch outcomes, one for each patch in the request + * associated with this response. + */ + public APIPatchResponse(List patches) { + this.patches = patches; + } + + /** + * Return the outcome for each patch in the request corresponding to this + * response. + */ + public List getPatches() { + return patches; + } +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java new file mode 100644 index 000000000..b5f824f63 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/jsonpatch/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Classes related to JSON Patch HTTP requests or responses. + * See https://www.rfc-editor.org/rfc/rfc6902. + */ +package org.apache.guacamole.rest.jsonpatch; diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java index 38b337e0e..62f4d0ed9 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java @@ -31,7 +31,7 @@ import org.apache.guacamole.net.auth.Permissions; import org.apache.guacamole.net.auth.permission.ObjectPermission; import org.apache.guacamole.net.auth.permission.Permission; import org.apache.guacamole.net.auth.permission.SystemPermission; -import org.apache.guacamole.rest.APIPatch; +import org.apache.guacamole.rest.jsonpatch.APIPatch; /** * A REST resource which abstracts the operations available on the permissions From e6bd12ee4c80b895ec00af81738f005ed254b3f5 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 24 Jan 2023 01:23:55 +0000 Subject: [PATCH 146/289] GUACAMOLE-926: Allow JDBC extensions to accept identifiers from user for update. --- .../ActiveConnectionService.java | 8 +++++ .../jdbc/base/DirectoryObjectService.java | 18 ++++++++++++ .../base/ModeledDirectoryObjectService.java | 14 +++++++++ .../guacamole/auth/jdbc/base/ObjectModel.java | 29 ++++++++++++++++++- .../jdbc/connection/ConnectionDirectory.java | 11 ++++--- .../auth/jdbc/connection/ConnectionModel.java | 5 ---- .../jdbc/connection/ConnectionService.java | 7 ++++- .../connectiongroup/ConnectionGroupModel.java | 6 ---- .../ConnectionGroupService.java | 7 ++++- .../sharingprofile/SharingProfileModel.java | 6 ---- .../sharingprofile/SharingProfileService.java | 7 ++++- .../guacamole/auth/jdbc/user/UserService.java | 4 +++ .../auth/jdbc/usergroup/UserGroupService.java | 4 +++ .../rest/directory/DirectoryResource.java | 16 ++++++++++ 14 files changed, 115 insertions(+), 27 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java index 046cee1e2..80c75dbbb 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java @@ -166,6 +166,14 @@ public class ActiveConnectionService } + @Override + public void updateExternalObject(ModeledAuthenticatedUser user, ActiveConnection object) throws GuacamoleException { + + // Updating active connections is not implemented + throw new GuacamoleSecurityException("Permission denied."); + + } + /** * Retrieve the permission set for the specified user that relates * to access to active connections. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java index 590e01e9f..29d40cc49 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java @@ -116,6 +116,24 @@ public interface DirectoryObjectService { void deleteObject(ModeledAuthenticatedUser user, String identifier) throws GuacamoleException; + /** + * Updates the object corresponding to the given external representation, + * applying any changes that have been made. If no such object exists, + * this function has no effect. + * + * @param user + * The user updating the object. + * + * @param object + * The external object to apply updates from. + * + * @throws GuacamoleException + * If the user lacks permission to update the object, or an error + * occurs while updating the object. + */ + void updateExternalObject(ModeledAuthenticatedUser user, ExternalType object) + throws GuacamoleException; + /** * Updates the given object, applying any changes that have been made. If * no such object exists, this function has no effect. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java index 80bc7eb5c..75c25926b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java @@ -511,6 +511,20 @@ public abstract class ModeledDirectoryObjectService getIdentifiers(ModeledAuthenticatedUser user) throws GuacamoleException { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java index c3052b1b4..69eb1fd36 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java @@ -21,6 +21,8 @@ package org.apache.guacamole.auth.jdbc.base; import java.util.Collection; +import org.apache.guacamole.GuacamoleException; + /** * Object representation of a Guacamole object, such as a user or connection, * as represented in the database. @@ -75,7 +77,7 @@ public abstract class ObjectModel { * * @return * The ID of this object in the database, or null if this object was - * not retrieved from the database. + * not retrieved from or intended to update the database. */ public Integer getObjectID() { return objectID; @@ -91,6 +93,31 @@ public abstract class ObjectModel { this.objectID = objectID; } + /** + * Given a text identifier, attempt to convert to an integer database ID. + * If the identifier is valid, the database ID will be set to this value. + * Otherwise, a GuacamoleException will be thrown. + * + * @param identifier + * The identifier to convert to an integer and set on the database + * model, if valid. + * + * @throws GuacamoleException + * If the provided identifier is not a valid integer. + */ + public void setObjectID(String identifier) throws GuacamoleException { + + // Try to convert the provided identifier to an integer ID + try { + setObjectID(Integer.parseInt(identifier)); + } + + catch (NumberFormatException e) { + throw new GuacamoleException( + "Database identifiers must be integers."); + } + } + /** * Returns a map of attribute name/value pairs for all attributes associated * with this model which do not have explicit mappings to actual model diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index 775628889..b2f3e2bea 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -71,16 +71,15 @@ public class ConnectionDirectory extends JDBCDirectory { // If the provided connection is already an internal type, update // using the internal method - if (object instanceof ModeledConnection) { - ModeledConnection connection = (ModeledConnection) object; - connectionService.updateObject(getCurrentUser(), connection); - } + if (object instanceof ModeledConnection) + connectionService.updateObject( + getCurrentUser(), (ModeledConnection) object); // If the type is not already the expected internal type, use the // external update method - else { + else connectionService.updateExternalObject(getCurrentUser(), object); - } + } @Override diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java index da454025d..03413650b 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java @@ -387,9 +387,4 @@ public class ConnectionModel extends ChildObjectModel { } - @Override - public void setIdentifier(String identifier) { - throw new UnsupportedOperationException("Connection identifiers are derived from IDs. They cannot be set."); - } - } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java index b3ed89ce8..cdf2afb76 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java @@ -110,14 +110,19 @@ public class ConnectionService extends ModeledChildDirectoryObjectService Date: Tue, 24 Jan 2023 19:20:03 +0000 Subject: [PATCH 147/289] GUACAMOLE-926: Parse YAML, CSV, JSON on frontend and submit to API. --- doc/licenses/base64-js-1.5.1/LICENSE | 22 + doc/licenses/base64-js-1.5.1/README | 8 + .../base64-js-1.5.1/dep-coordinates.txt | 1 + doc/licenses/buffer-4.9.2/LICENSE | 21 + doc/licenses/buffer-4.9.2/README | 7 + doc/licenses/buffer-4.9.2/dep-coordinates.txt | 1 + doc/licenses/core-util-is-1.0.3/LICENSE | 19 + doc/licenses/core-util-is-1.0.3/README | 7 + .../core-util-is-1.0.3/dep-coordinates.txt | 1 + doc/licenses/csv-6.2.5/LICENSE | 21 + doc/licenses/csv-6.2.5/README | 7 + doc/licenses/csv-6.2.5/dep-coordinates.txt | 2 + doc/licenses/events-3.3.0/LICENSE | 22 + doc/licenses/events-3.3.0/README | 7 + doc/licenses/events-3.3.0/dep-coordinates.txt | 1 + doc/licenses/ieee754-1.2.1/LICENSE | 11 + doc/licenses/ieee754-1.2.1/README | 7 + .../ieee754-1.2.1/dep-coordinates.txt | 1 + doc/licenses/inherits-2.0.4/LICENSE | 15 + doc/licenses/inherits-2.0.4/README | 8 + .../inherits-2.0.4/dep-coordinates.txt | 1 + doc/licenses/isarray-1.0.0/LICENSE | 21 + doc/licenses/isarray-1.0.0/README | 8 + .../isarray-1.0.0/dep-coordinates.txt | 1 + .../process-nextick-args-2.0.1/README | 8 + .../dep-coordinates.txt | 1 + .../process-nextick-args-2.0.1/license.md | 19 + doc/licenses/readable-stream-2.3.7/LICENSE | 47 + doc/licenses/readable-stream-2.3.7/README | 8 + .../readable-stream-2.3.7/dep-coordinates.txt | 1 + doc/licenses/safe-buffer-5.1.2/LICENSE | 21 + doc/licenses/safe-buffer-5.1.2/README | 8 + .../safe-buffer-5.1.2/dep-coordinates.txt | 1 + doc/licenses/setimmediate-1.0.5/LICENSE.txt | 20 + doc/licenses/setimmediate-1.0.5/README | 8 + .../setimmediate-1.0.5/dep-coordinates.txt | 1 + doc/licenses/stream-browserify-2.0.2/LICENSE | 20 + doc/licenses/stream-browserify-2.0.2/README | 8 + .../dep-coordinates.txt | 1 + doc/licenses/string_decoder-1.1.1/LICENSE | 47 + doc/licenses/string_decoder-1.1.1/README | 8 + .../string_decoder-1.1.1/dep-coordinates.txt | 1 + .../timers-browserify-2.0.12/LICENSE.md | 23 + doc/licenses/timers-browserify-2.0.12/README | 8 + .../dep-coordinates.txt | 1 + doc/licenses/util-deprecate-1.0.2/LICENSE | 24 + doc/licenses/util-deprecate-1.0.2/README | 8 + .../util-deprecate-1.0.2/dep-coordinates.txt | 1 + guacamole/src/main/frontend/package-lock.json | 3907 ++++++++++++++++- guacamole/src/main/frontend/package.json | 7 +- .../src/app/index/config/indexRouteConfig.js | 9 + .../app/rest/services/connectionService.js | 40 + .../src/app/rest/types/RelatedObjectPatch.js | 2 +- .../connectionHistoryPlayerController.js | 2 +- .../importConnectionsController.js | 78 + .../controllers/settingsController.js | 2 +- .../services/connectionImportParseService.js | 355 ++ .../src/app/settings/styles/buttons.css | 10 +- .../templates/settingsConnections.html | 4 + .../settings/templates/settingsImport.html | 11 + .../src/app/settings/types/ParseError.js | 59 + .../action-icons/guac-monitor-add-many.svg | 77 + .../main/frontend/src/translations/en.json | 10 +- guacamole/src/main/frontend/webpack.config.js | 16 + 64 files changed, 5093 insertions(+), 9 deletions(-) create mode 100644 doc/licenses/base64-js-1.5.1/LICENSE create mode 100644 doc/licenses/base64-js-1.5.1/README create mode 100644 doc/licenses/base64-js-1.5.1/dep-coordinates.txt create mode 100644 doc/licenses/buffer-4.9.2/LICENSE create mode 100644 doc/licenses/buffer-4.9.2/README create mode 100644 doc/licenses/buffer-4.9.2/dep-coordinates.txt create mode 100644 doc/licenses/core-util-is-1.0.3/LICENSE create mode 100644 doc/licenses/core-util-is-1.0.3/README create mode 100644 doc/licenses/core-util-is-1.0.3/dep-coordinates.txt create mode 100644 doc/licenses/csv-6.2.5/LICENSE create mode 100644 doc/licenses/csv-6.2.5/README create mode 100644 doc/licenses/csv-6.2.5/dep-coordinates.txt create mode 100644 doc/licenses/events-3.3.0/LICENSE create mode 100644 doc/licenses/events-3.3.0/README create mode 100644 doc/licenses/events-3.3.0/dep-coordinates.txt create mode 100644 doc/licenses/ieee754-1.2.1/LICENSE create mode 100644 doc/licenses/ieee754-1.2.1/README create mode 100644 doc/licenses/ieee754-1.2.1/dep-coordinates.txt create mode 100644 doc/licenses/inherits-2.0.4/LICENSE create mode 100644 doc/licenses/inherits-2.0.4/README create mode 100644 doc/licenses/inherits-2.0.4/dep-coordinates.txt create mode 100644 doc/licenses/isarray-1.0.0/LICENSE create mode 100644 doc/licenses/isarray-1.0.0/README create mode 100644 doc/licenses/isarray-1.0.0/dep-coordinates.txt create mode 100644 doc/licenses/process-nextick-args-2.0.1/README create mode 100644 doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt create mode 100644 doc/licenses/process-nextick-args-2.0.1/license.md create mode 100644 doc/licenses/readable-stream-2.3.7/LICENSE create mode 100644 doc/licenses/readable-stream-2.3.7/README create mode 100644 doc/licenses/readable-stream-2.3.7/dep-coordinates.txt create mode 100644 doc/licenses/safe-buffer-5.1.2/LICENSE create mode 100644 doc/licenses/safe-buffer-5.1.2/README create mode 100644 doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt create mode 100644 doc/licenses/setimmediate-1.0.5/LICENSE.txt create mode 100644 doc/licenses/setimmediate-1.0.5/README create mode 100644 doc/licenses/setimmediate-1.0.5/dep-coordinates.txt create mode 100644 doc/licenses/stream-browserify-2.0.2/LICENSE create mode 100644 doc/licenses/stream-browserify-2.0.2/README create mode 100644 doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt create mode 100644 doc/licenses/string_decoder-1.1.1/LICENSE create mode 100644 doc/licenses/string_decoder-1.1.1/README create mode 100644 doc/licenses/string_decoder-1.1.1/dep-coordinates.txt create mode 100644 doc/licenses/timers-browserify-2.0.12/LICENSE.md create mode 100644 doc/licenses/timers-browserify-2.0.12/README create mode 100644 doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt create mode 100644 doc/licenses/util-deprecate-1.0.2/LICENSE create mode 100644 doc/licenses/util-deprecate-1.0.2/README create mode 100644 doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt create mode 100644 guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js create mode 100644 guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js create mode 100644 guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html create mode 100644 guacamole/src/main/frontend/src/app/settings/types/ParseError.js create mode 100644 guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg diff --git a/doc/licenses/base64-js-1.5.1/LICENSE b/doc/licenses/base64-js-1.5.1/LICENSE new file mode 100644 index 000000000..9143d4cd6 --- /dev/null +++ b/doc/licenses/base64-js-1.5.1/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Jameson Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +Footer diff --git a/doc/licenses/base64-js-1.5.1/README b/doc/licenses/base64-js-1.5.1/README new file mode 100644 index 000000000..6cacd8fc3 --- /dev/null +++ b/doc/licenses/base64-js-1.5.1/README @@ -0,0 +1,8 @@ +base64-js (https://github.com/beatgammit/base64-js) +--------------------------------------------- + + Version: 1.5.1 + From: 'Jameson Little' (https://github.com/beatgammit/) + License(s): + MIT (bundled/base640-js-1.5.1/LICENSE) + diff --git a/doc/licenses/base64-js-1.5.1/dep-coordinates.txt b/doc/licenses/base64-js-1.5.1/dep-coordinates.txt new file mode 100644 index 000000000..1d66ee817 --- /dev/null +++ b/doc/licenses/base64-js-1.5.1/dep-coordinates.txt @@ -0,0 +1 @@ +base64-js:1.5.1 diff --git a/doc/licenses/buffer-4.9.2/LICENSE b/doc/licenses/buffer-4.9.2/LICENSE new file mode 100644 index 000000000..d6bf75dcf --- /dev/null +++ b/doc/licenses/buffer-4.9.2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh, and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/doc/licenses/buffer-4.9.2/README b/doc/licenses/buffer-4.9.2/README new file mode 100644 index 000000000..7caad56c9 --- /dev/null +++ b/doc/licenses/buffer-4.9.2/README @@ -0,0 +1,7 @@ +buffer (https://github.com/feross/buffer) +--------------------------------------------- + + Version: 4.9.2 + From: 'Feross Aboukhadijeh' (https://github.com/feross) + License(s): + MIT (bundled/buffer-4.9.2/LICENSE) diff --git a/doc/licenses/buffer-4.9.2/dep-coordinates.txt b/doc/licenses/buffer-4.9.2/dep-coordinates.txt new file mode 100644 index 000000000..30f193d0a --- /dev/null +++ b/doc/licenses/buffer-4.9.2/dep-coordinates.txt @@ -0,0 +1 @@ +buffer:4.9.2 diff --git a/doc/licenses/core-util-is-1.0.3/LICENSE b/doc/licenses/core-util-is-1.0.3/LICENSE new file mode 100644 index 000000000..d8d7f9437 --- /dev/null +++ b/doc/licenses/core-util-is-1.0.3/LICENSE @@ -0,0 +1,19 @@ +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/doc/licenses/core-util-is-1.0.3/README b/doc/licenses/core-util-is-1.0.3/README new file mode 100644 index 000000000..411c1f1e9 --- /dev/null +++ b/doc/licenses/core-util-is-1.0.3/README @@ -0,0 +1,7 @@ +core-util-is (https://github.com/isaacs/core-util-is) +--------------------------------------------- + + Version: 1.0.3 + From: 'isaacs' (https://github.com/isaacs) + License(s): + MIT (bundled/core-util-is-1.0.3/LICENSE) diff --git a/doc/licenses/core-util-is-1.0.3/dep-coordinates.txt b/doc/licenses/core-util-is-1.0.3/dep-coordinates.txt new file mode 100644 index 000000000..f4b1c7a8f --- /dev/null +++ b/doc/licenses/core-util-is-1.0.3/dep-coordinates.txt @@ -0,0 +1 @@ +core-util-is:1.0.3 diff --git a/doc/licenses/csv-6.2.5/LICENSE b/doc/licenses/csv-6.2.5/LICENSE new file mode 100644 index 000000000..918eaf05a --- /dev/null +++ b/doc/licenses/csv-6.2.5/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010 Adaltas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/csv-6.2.5/README b/doc/licenses/csv-6.2.5/README new file mode 100644 index 000000000..5fb35fae4 --- /dev/null +++ b/doc/licenses/csv-6.2.5/README @@ -0,0 +1,7 @@ +node-csv (https://github.com/adaltas/node-csv) +--------------------------------------------- + + Version: 6.2.5 + From: 'adaltas' (https://github.com/adaltas) + License(s): + MIT (bundled/csv-6.2.5/LICENSE) diff --git a/doc/licenses/csv-6.2.5/dep-coordinates.txt b/doc/licenses/csv-6.2.5/dep-coordinates.txt new file mode 100644 index 000000000..dc0f22695 --- /dev/null +++ b/doc/licenses/csv-6.2.5/dep-coordinates.txt @@ -0,0 +1,2 @@ +csv:6.2.5 +csv-parse:5.3.3 diff --git a/doc/licenses/events-3.3.0/LICENSE b/doc/licenses/events-3.3.0/LICENSE new file mode 100644 index 000000000..52ed3b0a6 --- /dev/null +++ b/doc/licenses/events-3.3.0/LICENSE @@ -0,0 +1,22 @@ +MIT + +Copyright Joyent, Inc. and other Node contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/events-3.3.0/README b/doc/licenses/events-3.3.0/README new file mode 100644 index 000000000..274b952ab --- /dev/null +++ b/doc/licenses/events-3.3.0/README @@ -0,0 +1,7 @@ +events (https://github.com/browserify/events) +--------------------------------------------- + + Version: 3.3.0 + From: 'browserify' (https://github.com/browserify) + License(s): + MIT (bundled/events-3.3.0/LICENSE) diff --git a/doc/licenses/events-3.3.0/dep-coordinates.txt b/doc/licenses/events-3.3.0/dep-coordinates.txt new file mode 100644 index 000000000..f644fcc1b --- /dev/null +++ b/doc/licenses/events-3.3.0/dep-coordinates.txt @@ -0,0 +1 @@ +events:3.3.0 diff --git a/doc/licenses/ieee754-1.2.1/LICENSE b/doc/licenses/ieee754-1.2.1/LICENSE new file mode 100644 index 000000000..5aac82c78 --- /dev/null +++ b/doc/licenses/ieee754-1.2.1/LICENSE @@ -0,0 +1,11 @@ +Copyright 2008 Fair Oaks Labs, Inc. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/doc/licenses/ieee754-1.2.1/README b/doc/licenses/ieee754-1.2.1/README new file mode 100644 index 000000000..06d856f16 --- /dev/null +++ b/doc/licenses/ieee754-1.2.1/README @@ -0,0 +1,7 @@ +ieee754 (https://github.com/feross/ieee754) +--------------------------------------------- + + Version: 1.2.1 + From: 'Feross Aboukhadijeh' (https://github.com/feross) + License(s): + MIT (bundled/ieee754-1.2.1/LICENSE) diff --git a/doc/licenses/ieee754-1.2.1/dep-coordinates.txt b/doc/licenses/ieee754-1.2.1/dep-coordinates.txt new file mode 100644 index 000000000..4fb7d7765 --- /dev/null +++ b/doc/licenses/ieee754-1.2.1/dep-coordinates.txt @@ -0,0 +1 @@ +ieee754:1.2.1 diff --git a/doc/licenses/inherits-2.0.4/LICENSE b/doc/licenses/inherits-2.0.4/LICENSE new file mode 100644 index 000000000..052085c43 --- /dev/null +++ b/doc/licenses/inherits-2.0.4/LICENSE @@ -0,0 +1,15 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/doc/licenses/inherits-2.0.4/README b/doc/licenses/inherits-2.0.4/README new file mode 100644 index 000000000..9d0b40126 --- /dev/null +++ b/doc/licenses/inherits-2.0.4/README @@ -0,0 +1,8 @@ +inherits (https://github.com/isaacs/inherits) +--------------------------------------------- + + Version: 2.0.4 + From: 'Isaac Z. Schlueter' (https://github.com/isaacs) + License(s): + ISC (bundled/inherits-2.0.4/LICENSE) + diff --git a/doc/licenses/inherits-2.0.4/dep-coordinates.txt b/doc/licenses/inherits-2.0.4/dep-coordinates.txt new file mode 100644 index 000000000..4a17c13e3 --- /dev/null +++ b/doc/licenses/inherits-2.0.4/dep-coordinates.txt @@ -0,0 +1 @@ +inherits:2.0.4 diff --git a/doc/licenses/isarray-1.0.0/LICENSE b/doc/licenses/isarray-1.0.0/LICENSE new file mode 100644 index 000000000..de3226673 --- /dev/null +++ b/doc/licenses/isarray-1.0.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/licenses/isarray-1.0.0/README b/doc/licenses/isarray-1.0.0/README new file mode 100644 index 000000000..3fde38c8c --- /dev/null +++ b/doc/licenses/isarray-1.0.0/README @@ -0,0 +1,8 @@ +isarray (https://github.com/juliangruber/isarray) +--------------------------------------------- + + Version: 1.0.0 + From: 'Julian Gruber' (https://github.com/juliangruber) + License(s): + MIT (bundled/isarray-1.0.0/LICENSE) + diff --git a/doc/licenses/isarray-1.0.0/dep-coordinates.txt b/doc/licenses/isarray-1.0.0/dep-coordinates.txt new file mode 100644 index 000000000..d444bdbd9 --- /dev/null +++ b/doc/licenses/isarray-1.0.0/dep-coordinates.txt @@ -0,0 +1 @@ +isarray:1.0.0 diff --git a/doc/licenses/process-nextick-args-2.0.1/README b/doc/licenses/process-nextick-args-2.0.1/README new file mode 100644 index 000000000..d7c0f6b3b --- /dev/null +++ b/doc/licenses/process-nextick-args-2.0.1/README @@ -0,0 +1,8 @@ +process-nextick-args (https://github.com/calvinmetcalf/process-nextick-args) +--------------------------------------------- + + Version: 2.0.1 + From: 'Calvin Metcalf' (https://github.com/calvinmetcalf) + License(s): + MIT (bundled/process-nextick-args-2.0.1/LICENSE) + diff --git a/doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt b/doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt new file mode 100644 index 000000000..7c68c4b48 --- /dev/null +++ b/doc/licenses/process-nextick-args-2.0.1/dep-coordinates.txt @@ -0,0 +1 @@ +process-nextick-args:2.0.1 diff --git a/doc/licenses/process-nextick-args-2.0.1/license.md b/doc/licenses/process-nextick-args-2.0.1/license.md new file mode 100644 index 000000000..c67e3532b --- /dev/null +++ b/doc/licenses/process-nextick-args-2.0.1/license.md @@ -0,0 +1,19 @@ +# Copyright (c) 2015 Calvin Metcalf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.** diff --git a/doc/licenses/readable-stream-2.3.7/LICENSE b/doc/licenses/readable-stream-2.3.7/LICENSE new file mode 100644 index 000000000..2873b3b2e --- /dev/null +++ b/doc/licenses/readable-stream-2.3.7/LICENSE @@ -0,0 +1,47 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" diff --git a/doc/licenses/readable-stream-2.3.7/README b/doc/licenses/readable-stream-2.3.7/README new file mode 100644 index 000000000..cb58f728b --- /dev/null +++ b/doc/licenses/readable-stream-2.3.7/README @@ -0,0 +1,8 @@ +readable-stream (https://github.com/nodejs/readable-stream) +--------------------------------------------- + + Version: 2.3.7 + From: 'Node.js' (https://github.com/nodejs) + License(s): + MIT (bundled/readable-stream-2.3.7/LICENSE) + diff --git a/doc/licenses/readable-stream-2.3.7/dep-coordinates.txt b/doc/licenses/readable-stream-2.3.7/dep-coordinates.txt new file mode 100644 index 000000000..f645a21bb --- /dev/null +++ b/doc/licenses/readable-stream-2.3.7/dep-coordinates.txt @@ -0,0 +1 @@ +readable-stream:2.3.7 diff --git a/doc/licenses/safe-buffer-5.1.2/LICENSE b/doc/licenses/safe-buffer-5.1.2/LICENSE new file mode 100644 index 000000000..0c068ceec --- /dev/null +++ b/doc/licenses/safe-buffer-5.1.2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Feross Aboukhadijeh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/doc/licenses/safe-buffer-5.1.2/README b/doc/licenses/safe-buffer-5.1.2/README new file mode 100644 index 000000000..26cf5dc2f --- /dev/null +++ b/doc/licenses/safe-buffer-5.1.2/README @@ -0,0 +1,8 @@ +safe-buffer (https://github.com/feross/safe-buffer) +--------------------------------------------- + + Version: 5.1.2 + From: 'Feross Aboukhadijeh' (https://github.com/feross) + License(s): + MIT (bundled/safe-buffer-5.1.2/LICENSE) + diff --git a/doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt b/doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt new file mode 100644 index 000000000..594ab08c8 --- /dev/null +++ b/doc/licenses/safe-buffer-5.1.2/dep-coordinates.txt @@ -0,0 +1 @@ +safe-buffer:5.1.2 diff --git a/doc/licenses/setimmediate-1.0.5/LICENSE.txt b/doc/licenses/setimmediate-1.0.5/LICENSE.txt new file mode 100644 index 000000000..32b20de6a --- /dev/null +++ b/doc/licenses/setimmediate-1.0.5/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/setimmediate-1.0.5/README b/doc/licenses/setimmediate-1.0.5/README new file mode 100644 index 000000000..0fe5638ee --- /dev/null +++ b/doc/licenses/setimmediate-1.0.5/README @@ -0,0 +1,8 @@ +setImmediate.js (https://github.com/YuzuJS/setImmediate) +--------------------------------------------- + + Version: 1.0.5 + From: 'Yuzu (by Barnes & Noble Education)' (https://github.com/YuzuJS) + License(s): + MIT (bundled/setimmediate-1.0.5/LICENSE) + diff --git a/doc/licenses/setimmediate-1.0.5/dep-coordinates.txt b/doc/licenses/setimmediate-1.0.5/dep-coordinates.txt new file mode 100644 index 000000000..d2111b4e0 --- /dev/null +++ b/doc/licenses/setimmediate-1.0.5/dep-coordinates.txt @@ -0,0 +1 @@ +setimmediate:1.0.5 diff --git a/doc/licenses/stream-browserify-2.0.2/LICENSE b/doc/licenses/stream-browserify-2.0.2/LICENSE new file mode 100644 index 000000000..3e7d0c01e --- /dev/null +++ b/doc/licenses/stream-browserify-2.0.2/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/stream-browserify-2.0.2/README b/doc/licenses/stream-browserify-2.0.2/README new file mode 100644 index 000000000..8ab34a47f --- /dev/null +++ b/doc/licenses/stream-browserify-2.0.2/README @@ -0,0 +1,8 @@ +stream-browserify (https://github.com/browserify/stream-browserify) +--------------------------------------------- + + Version: 2.0.2 + From: 'browserify' (https://github.com/browserify) + License(s): + MIT (bundled/stream-browserify-2.0.2/LICENSE) + diff --git a/doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt b/doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt new file mode 100644 index 000000000..7bc88e94d --- /dev/null +++ b/doc/licenses/stream-browserify-2.0.2/dep-coordinates.txt @@ -0,0 +1 @@ +stream-browserify:2.0.2 diff --git a/doc/licenses/string_decoder-1.1.1/LICENSE b/doc/licenses/string_decoder-1.1.1/LICENSE new file mode 100644 index 000000000..2873b3b2e --- /dev/null +++ b/doc/licenses/string_decoder-1.1.1/LICENSE @@ -0,0 +1,47 @@ +Node.js is licensed for use as follows: + +""" +Copyright Node.js contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" + +This license applies to parts of Node.js originating from the +https://github.com/joyent/node repository: + +""" +Copyright Joyent, Inc. and other Node contributors. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. +""" diff --git a/doc/licenses/string_decoder-1.1.1/README b/doc/licenses/string_decoder-1.1.1/README new file mode 100644 index 000000000..a64b3299f --- /dev/null +++ b/doc/licenses/string_decoder-1.1.1/README @@ -0,0 +1,8 @@ +string_decoder (https://github.com/nodejs/string_decoder) +--------------------------------------------- + + Version: 1.1.1 + From: 'Node.js' (https://github.com/nodejs) + License(s): + MIT (bundled/string_decoder-1.1.1/LICENSE) + diff --git a/doc/licenses/string_decoder-1.1.1/dep-coordinates.txt b/doc/licenses/string_decoder-1.1.1/dep-coordinates.txt new file mode 100644 index 000000000..bbdc75db5 --- /dev/null +++ b/doc/licenses/string_decoder-1.1.1/dep-coordinates.txt @@ -0,0 +1 @@ +string_decoder:1.1.1 diff --git a/doc/licenses/timers-browserify-2.0.12/LICENSE.md b/doc/licenses/timers-browserify-2.0.12/LICENSE.md new file mode 100644 index 000000000..f2bdf520a --- /dev/null +++ b/doc/licenses/timers-browserify-2.0.12/LICENSE.md @@ -0,0 +1,23 @@ +# timers-browserify + +This project uses the [MIT](http://jryans.mit-license.org/) license: + + Copyright © 2012 J. Ryan Stinnett + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the “Software”), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/timers-browserify-2.0.12/README b/doc/licenses/timers-browserify-2.0.12/README new file mode 100644 index 000000000..0e1eb4e8a --- /dev/null +++ b/doc/licenses/timers-browserify-2.0.12/README @@ -0,0 +1,8 @@ +timers-browserify (https://github.com/browserify/timers-browserify) +--------------------------------------------- + + Version: 2.0.12 + From: 'browserify' (https://github.com/browserify) + License(s): + MIT (bundled/timers-browserify-2.0.12/LICENSE) + diff --git a/doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt b/doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt new file mode 100644 index 000000000..b1dfbee66 --- /dev/null +++ b/doc/licenses/timers-browserify-2.0.12/dep-coordinates.txt @@ -0,0 +1 @@ +timers-browserify:2.0.12 diff --git a/doc/licenses/util-deprecate-1.0.2/LICENSE b/doc/licenses/util-deprecate-1.0.2/LICENSE new file mode 100644 index 000000000..6a60e8c22 --- /dev/null +++ b/doc/licenses/util-deprecate-1.0.2/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2014 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/licenses/util-deprecate-1.0.2/README b/doc/licenses/util-deprecate-1.0.2/README new file mode 100644 index 000000000..93844707b --- /dev/null +++ b/doc/licenses/util-deprecate-1.0.2/README @@ -0,0 +1,8 @@ +util-deprecate (https://github.com/TooTallNate/util-deprecate) +--------------------------------------------- + + Version: 1.0.2 + From: 'Nathan Rajlich' (https://github.com/TooTallNate) + License(s): + MIT (bundled/util-deprecate-1.0.2/LICENSE) + diff --git a/doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt b/doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt new file mode 100644 index 000000000..f8c30242a --- /dev/null +++ b/doc/licenses/util-deprecate-1.0.2/dep-coordinates.txt @@ -0,0 +1 @@ +util-deprecate:1.0.2 diff --git a/guacamole/src/main/frontend/package-lock.json b/guacamole/src/main/frontend/package-lock.json index a9f041274..32b9bee38 100644 --- a/guacamole/src/main/frontend/package-lock.json +++ b/guacamole/src/main/frontend/package-lock.json @@ -12,14 +12,19 @@ "angular-translate": "^2.19.0", "angular-translate-interpolation-messageformat": "^2.19.0", "angular-translate-loader-static-files": "^2.19.0", - "blob-polyfill": "^7.0.20220408", + "blob-polyfill": ">=7.0.20220408", + "csv": "^6.2.5", "datalist-polyfill": "^1.25.1", "file-saver": "^2.0.5", "jquery": "^3.6.4", "jstz": "^2.1.1", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "yaml": "^2.2.1" }, "devDependencies": { + "@babel/core": "^7.20.12", + "@babel/preset-env": "^7.20.2", + "babel-loader": "^8.3.0", "clean-webpack-plugin": "^4.0.0", "closure-webpack-plugin": "^2.6.1", "copy-webpack-plugin": "^5.1.2", @@ -34,6 +39,1797 @@ "webpack-cli": "^4.10.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.20.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", + "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", + "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", + "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz", + "integrity": "sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.2.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", + "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", + "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.13", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", + "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz", + "integrity": "sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz", + "integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz", + "integrity": "sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", + "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz", + "integrity": "sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz", + "integrity": "sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", + "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", + "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz", + "integrity": "sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-simple-access": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", + "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", + "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.13", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -49,6 +1845,53 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -766,6 +2609,216 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/babel-loader/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/babel-loader/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-loader/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1703,6 +3756,12 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -1786,6 +3845,19 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-js-compat": { + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz", + "integrity": "sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2544,6 +4616,35 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, + "node_modules/csv": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz", + "integrity": "sha512-T+K0H7MIrlrnP6KxYKo3lK+uLl6OC2Gmwdd81TG/VdkhKvpatl35sR7tyRSpDLGl22y2T+q9KvNHnVtn4OAscQ==", + "dependencies": { + "csv-generate": "^4.2.1", + "csv-parse": "^5.3.3", + "csv-stringify": "^6.2.3", + "stream-transform": "^3.2.1" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.1.tgz", + "integrity": "sha512-w6GFHjvApv6bcJ2xdi9JGsH6ZvUBfC+vUdfefnEzurXG6hMRwzkBLnhztU2H7v7+zfCk1I/knnQ+tGbgpxWrBw==" + }, + "node_modules/csv-parse": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.3.tgz", + "integrity": "sha512-kEWkAPleNEdhFNkHQpFHu9RYPogsFj3dx6bCxL847fsiLgidzWg0z/O0B1kVWMJUc5ky64zGp18LX2T3DQrOfw==" + }, + "node_modules/csv-stringify": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.2.3.tgz", + "integrity": "sha512-4qGjUMwnlaRc00gc2jrIYh2w/h1fo25B0mTuY9K8fBiIgtmCX3LcgUbrEGViL98Ci4Se/F5LFEtu8k+dItJVZQ==" + }, "node_modules/cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -3045,6 +5146,15 @@ "node": ">=4.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -3522,6 +5632,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -3590,6 +5709,15 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -4646,6 +6774,12 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -4750,6 +6884,12 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7202,6 +9342,39 @@ "node": ">= 0.10" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, "node_modules/regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -7231,6 +9404,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", + "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -7975,6 +10192,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "node_modules/stream-transform": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.1.tgz", + "integrity": "sha512-ApK+WTJ5bCOf0A2tlec1qhvr8bGEBM/sgXXB7mysdCYgZJO5DZeaV3h3G+g0HnAQ372P5IhiGqnW29zoLOfTzQ==" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8399,6 +10621,15 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -8492,6 +10723,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -9147,6 +11418,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "node_modules/yaml": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -9161,6 +11440,1261 @@ } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.20.10", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.10.tgz", + "integrity": "sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg==", + "dev": true + }, + "@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", + "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz", + "integrity": "sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz", + "integrity": "sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.2.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz", + "integrity": "sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "requires": { + "@babel/types": "^7.20.2" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } + }, + "@babel/helpers": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", + "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.13", + "@babel/types": "^7.20.7" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", + "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.20.7.tgz", + "integrity": "sha512-AveGOoi9DAjUYYuUAG//Ig69GlazLnoyzMw68VCDux+c1tsnnH/OkYcpz/5xzMkEFC6UxjR5Gw1c+iY2wOGVeQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz", + "integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz", + "integrity": "sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", + "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.11.tgz", + "integrity": "sha512-tA4N427a7fjf1P0/2I4ScsHGc5jcHPbb30xMbaTke2gxDuWpUfXDuX1FEymJwKk4tuGUvGcejAR6HdZVqmmPyw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.7.tgz", + "integrity": "sha512-LWYbsiXTPKl+oBlXUGlwNlJZetXD5Am+CyBdqhPsDVjM9Jc8jwBJFrKhHf900Kfk2eZG1y9MAG3UNajol7A4VQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", + "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.7.tgz", + "integrity": "sha512-Xwg403sRrZb81IVB79ZPqNQME23yhugYVqgTxAhT99h485F4f+GMELFhhOsscDUB7HCswepKeCKLn/GZvUKoBA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.20.11.tgz", + "integrity": "sha512-S8e1f7WQ7cimJQ51JkAaDrEtohVEitXjgCGAS2N8S31Y42E+kWwfSz83LYz57QdBm7q9diARVqanIaH2oVgQnw==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-simple-access": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.7.tgz", + "integrity": "sha512-WiWBIkeHKVOSYPO0pWkxGPfKeWrCJyD3NJ53+Lrp/QMSZbsVPovrVl2aWZ19D/LTVnaDv5Ap7GJ/B2CTOZdrfA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", + "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.1", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.20.1", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.2", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.20.2", + "@babel/plugin-transform-classes": "^7.20.2", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.20.2", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.19.6", + "@babel/plugin-transform-modules-commonjs": "^7.19.6", + "@babel/plugin-transform-modules-systemjs": "^7.19.6", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.20.1", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.19.0", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.20.2", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/traverse": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", + "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.13", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -9173,6 +12707,44 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, "@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -9775,6 +13347,155 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "babel-loader": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", + "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -10518,6 +14239,12 @@ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -10584,6 +14311,15 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.0.tgz", "integrity": "sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg==" }, + "core-js-compat": { + "version": "3.27.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz", + "integrity": "sha512-welaYuF7ZtbYKGrIy7y3eb40d37rG1FvzEOfe7hSLd2iD6duMDqUhRfSvCGyC46HhR6Y8JXXdZ2lnRUMkPBpvg==", + "dev": true, + "requires": { + "browserslist": "^4.21.4" + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -11160,6 +14896,32 @@ } } }, + "csv": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz", + "integrity": "sha512-T+K0H7MIrlrnP6KxYKo3lK+uLl6OC2Gmwdd81TG/VdkhKvpatl35sR7tyRSpDLGl22y2T+q9KvNHnVtn4OAscQ==", + "requires": { + "csv-generate": "^4.2.1", + "csv-parse": "^5.3.3", + "csv-stringify": "^6.2.3", + "stream-transform": "^3.2.1" + } + }, + "csv-generate": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.1.tgz", + "integrity": "sha512-w6GFHjvApv6bcJ2xdi9JGsH6ZvUBfC+vUdfefnEzurXG6hMRwzkBLnhztU2H7v7+zfCk1I/knnQ+tGbgpxWrBw==" + }, + "csv-parse": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.3.tgz", + "integrity": "sha512-kEWkAPleNEdhFNkHQpFHu9RYPogsFj3dx6bCxL847fsiLgidzWg0z/O0B1kVWMJUc5ky64zGp18LX2T3DQrOfw==" + }, + "csv-stringify": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.2.3.tgz", + "integrity": "sha512-4qGjUMwnlaRc00gc2jrIYh2w/h1fo25B0mTuY9K8fBiIgtmCX3LcgUbrEGViL98Ci4Se/F5LFEtu8k+dItJVZQ==" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -11563,6 +15325,12 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -11935,6 +15703,12 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-intrinsic": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", @@ -11982,6 +15756,12 @@ "is-glob": "^4.0.1" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -12735,6 +16515,12 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==" }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -12812,6 +16598,12 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -14783,6 +18575,36 @@ "resolve": "^1.9.0" } }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -14803,6 +18625,43 @@ "functions-have-names": "^1.2.2" } }, + "regexpu-core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", + "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", + "dev": true, + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsgen": "^0.7.1", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", + "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", + "dev": true + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -15414,6 +19273,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "stream-transform": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.1.tgz", + "integrity": "sha512-ApK+WTJ5bCOf0A2tlec1qhvr8bGEBM/sgXXB7mysdCYgZJO5DZeaV3h3G+g0HnAQ372P5IhiGqnW29zoLOfTzQ==" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -15753,6 +19617,12 @@ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -15830,6 +19700,34 @@ "which-boxed-primitive": "^1.0.2" } }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -16342,6 +20240,11 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "yaml": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==" + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/guacamole/src/main/frontend/package.json b/guacamole/src/main/frontend/package.json index cb4982da0..617cf308b 100644 --- a/guacamole/src/main/frontend/package.json +++ b/guacamole/src/main/frontend/package.json @@ -12,13 +12,18 @@ "angular-translate-interpolation-messageformat": "^2.19.0", "angular-translate-loader-static-files": "^2.19.0", "blob-polyfill": ">=7.0.20220408", + "csv": "^6.2.5", "datalist-polyfill": "^1.25.1", "file-saver": "^2.0.5", "jquery": "^3.6.4", "jstz": "^2.1.1", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "yaml": "^2.2.1" }, "devDependencies": { + "@babel/core": "^7.20.12", + "@babel/preset-env": "^7.20.2", + "babel-loader": "^8.3.0", "clean-webpack-plugin": "^4.0.0", "closure-webpack-plugin": "^2.6.1", "copy-webpack-plugin": "^5.1.2", diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index d1e78754c..51c423932 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -126,6 +126,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider', resolve : { routeToUserHomePage: routeToUserHomePage } }) + // Connection import page + .when('/settings/:dataSource/import', { + title : 'APP.NAME', + bodyClassName : 'settings', + templateUrl : 'app/settings/templates/settingsImport.html', + controller : 'importConnectionsController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + // Management screen .when('/settings/:dataSource?/:tab', { title : 'APP.NAME', diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index 3531177ea..a91c8d05d 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -154,6 +154,46 @@ angular.module('rest').factory('connectionService', ['$injector', } }; + + /** + * Makes a request to the REST API to create multiple connections, returning a + * a promise that can be used for processing the results of the call. This + * operation is atomic - if any errors are encountered during the connection + * creation process, the entire request will fail, and no connections will be + * created. + * + * @param {Connection[]} connections The connections to create. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * create operation is successful. + */ + service.createConnections = function createConnections(dataSource, connections) { + + // An object containing a PATCH operation to create each connection + const patchBody = connections.map(connection => ({ + op: "add", + path: "/", + value: connection + })); + + // Make a PATCH request to create the connections + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', + data : patchBody + }) + + // Clear the cache + .then(function connectionUpdated(){ + cacheService.connections.removeAll(); + + // Clear users cache to force reload of permissions for this + // newly updated connection + cacheService.users.removeAll(); + }); + + } /** * Makes a request to the REST API to delete a connection, diff --git a/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js b/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js index bb82def73..c06493816 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js +++ b/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js @@ -82,4 +82,4 @@ angular.module('rest').factory('RelatedObjectPatch', [function defineRelatedObje return RelatedObjectPatch; -}]); \ No newline at end of file +}]); diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js b/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js index b40f5f93a..12c368be3 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js +++ b/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js @@ -20,7 +20,7 @@ /** * The controller for the session recording player page. */ -angular.module('manage').controller('connectionHistoryPlayerController', ['$scope', '$injector', +angular.module('settings').controller('connectionHistoryPlayerController', ['$scope', '$injector', function connectionHistoryPlayerController($scope, $injector) { // Required services diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js new file mode 100644 index 000000000..33b8914ad --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js @@ -0,0 +1,78 @@ +/* + * 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. + */ + +/** + * The controller for the connection import page. + */ +angular.module('settings').controller('importConnectionsController', ['$scope', '$injector', + function importConnectionsController($scope, $injector) { + + // Required services + const connectionImportParseService = $injector.get('connectionImportParseService'); + const connectionService = $injector.get('connectionService'); + + function processData(type, data) { + + let requestBody; + + // Parse the data based on the provided mimetype + switch(type) { + + case "application/json": + case "text/json": + requestBody = connectionImportParseService.parseJSON(data); + break; + + case "text/csv": + requestBody = connectionImportParseService.parseCSV(data); + break; + + case "application/yaml": + case "application/x-yaml": + case "text/yaml": + case "text/x-yaml": + requestBody = connectionImportParseService.parseYAML(data); + break; + + } + + console.log(requestBody); + } + + $scope.upload = function() { + + const files = angular.element('#file')[0].files; + + if (files.length <= 0) { + console.error("TODO: This should be a proper error tho"); + return; + } + + // The file that the user uploaded + const file = files[0]; + + // Call processData when the data is ready + const reader = new FileReader(); + reader.onloadend = (e => processData(file.type, e.target.result)); + + // Read all the data into memory and call processData when done + reader.readAsBinaryString(file); + } + +}]); diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js b/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js index a462d8715..9961d292f 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js +++ b/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js @@ -20,7 +20,7 @@ /** * The controller for the general settings page. */ -angular.module('manage').controller('settingsController', ['$scope', '$injector', +angular.module('settings').controller('settingsController', ['$scope', '$injector', function settingsController($scope, $injector) { // Required services diff --git a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js b/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js new file mode 100644 index 000000000..f2735acd6 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js @@ -0,0 +1,355 @@ +/* + * 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. + */ + +/* global _ */ + +import { parse as parseCSVData } from 'csv-parse/lib/sync' +import { parse as parseYAMLData } from 'yaml' + +/** + * A service for parsing user-provided JSON, YAML, or JSON connection data into + * an appropriate format for bulk uploading using the PATCH REST endpoint. + */ +angular.module('settings').factory('connectionImportParseService', + ['$injector', function connectionImportParseService($injector) { + + // Required types + const Connection = $injector.get('Connection'); + const ParseError = $injector.get('ParseError'); + const TranslatableMessage = $injector.get('TranslatableMessage'); + + // Required services + const $q = $injector.get('$q'); + const $routeParams = $injector.get('$routeParams'); + const schemaService = $injector.get('schemaService'); + const connectionGroupService = $injector.get('connectionGroupService'); + + const service = {}; + + /** + * Perform basic checks, common to all file types - namely that the parsed + * data is an array, and contains at least one connection entry. + * + * @throws {ParseError} + * An error describing the parsing failure, if one of the basic checks + * fails. + */ + function performBasicChecks(parsedData) { + + // Make sure that the file data parses to an array (connection list) + if (!(parsedData instanceof Array)) + throw new ParseError({ + message: 'Import data must be a list of connections', + translatableMessage: new TranslatableMessage({ + key: 'SETTINGS_CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' + }) + }); + + // Make sure that the connection list is not empty - contains at least + // one connection + if (!parsedData.length) + throw new ParseError({ + message: 'The provided CSV file is empty', + translatableMessage: new TranslatableMessage({ + key: 'SETTINGS_CONNECTION_IMPORT.ERROR_EMPTY_FILE' + }) + }); + } + + /** + * Convert a provided YAML representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * @param {String} yamlData + * The YAML-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseYAML = function parseYAML(yamlData) { + + // Parse from YAML into a javascript array + const parsedData = parseYAMLData(yamlData); + + // Check that the data is the correct format, and not empty + performBasicChecks(parsedData); + + // Convert to an array of Connection objects and return + const deferredConnections = $q.defer(); + deferredConnections.resolve( + parsedData.map(connection => new Connection(connection))); + return deferredConnections.promise; + + }; + + /** + * Returns a promise that resolves to an object detailing the connection + * attributes for the current data source, as well as the connection + * paremeters for every protocol, for the current data source. + * + * The object that the promise will contain an "attributes" key that maps to + * a set of attribute names, and a "protocolParameters" key that maps to an + * object mapping protocol names to sets of parameter names for that protocol. + * + * The intended use case for this object is to determine if there is a + * connection parameter or attribute with a given name, by e.g. checking the + * path `.protocolParameters[protocolName]` to see if a protocol exists, + * checking the path `.protocolParameters[protocolName][fieldName]` to see + * if a parameter exists for a given protocol, or checking the path + * `.attributes[fieldName]` to check if a connection attribute exists. + * + * @returns {Promise.} + */ + function getFieldLookups() { + + // The current data source - the one that the connections will be + // imported into + const dataSource = $routeParams.dataSource; + + // Fetch connection attributes and protocols for the current data source + return $q.all({ + attributes : schemaService.getConnectionAttributes(dataSource), + protocols : schemaService.getProtocols(dataSource) + }) + .then(function connectionStructureRetrieved({attributes, protocols}) { + + return { + + // Translate the forms and fields into a flat map of attribute + // name to `true` boolean value + attributes: attributes.reduce( + (attributeMap, form) => { + form.fields.forEach( + field => attributeMap[field.name] = true); + return attributeMap + }, {}), + + // Translate the protocol definitions into a map of protocol + // name to map of field name to `true` boolean value + protocolParameters: _.mapValues( + protocols, protocol => protocol.connectionForms.reduce( + (protocolFieldMap, form) => { + form.fields.forEach( + field => protocolFieldMap[field.name] = true); + return protocolFieldMap; + }, {})) + }; + }); + + } + + + /** + * Returns a promise that resolves to an object mapping potential groups + * that might be encountered in an imported connection to group identifiers. + * + * The idea is that a user-provided import file might directly specify a + * parentIdentifier, or it might specify a named group path like "ROOT", + * "ROOT/parent", or "ROOT/parent/child". This object resolved by the + * promise returned from this function will map all of the above to the + * identifier of the appropriate group, if defined. + * + * @returns {Promise.} + */ + function getGroupLookups() { + + // The current data source - defines all the groups that the connections + // might be imported into + const dataSource = $routeParams.dataSource; + + const deferredGroupLookups = $q.defer(); + + connectionGroupService.getConnectionGroupTree(dataSource).then( + rootGroup => { + + const groupLookup = {}; + + // Add the specified group to the lookup, appending all specified + // prefixes, and then recursively call saveLookups for all children + // of the group, appending to the prefix for each level + function saveLookups(prefix, group) { + + // To get the path for the current group, add the name + const currentPath = prefix + group.name; + + // Add the current path to the lookup + groupLookup[currentPath] = group.identifier; + + // Add each child group to the lookup + const nextPrefix = currentPath + "/"; + _.forEach(group.childConnectionGroups, + childGroup => saveLookups(nextPrefix, childGroup)); + } + + // Start at the root group + saveLookups("", rootGroup); + + // Resolve with the now fully-populated lookups + deferredGroupLookups.resolve(groupLookup); + + }); + + return deferredGroupLookups.promise; + } + +/* +// Example Connection JSON +{ + "attributes": { + "failover-only": "true", + "guacd-encryption": "none", + "guacd-hostname": "potato", + "guacd-port": "1234", + "ksm-user-config-enabled": "true", + "max-connections": "1", + "max-connections-per-user": "1", + "weight": "1" + }, + "name": "Bloatato", + "parameters": { + "audio-servername": "heyoooooooo", + "clipboard-encoding": "", + "color-depth": "", + "create-recording-path": "", + "cursor": "remote", + "dest-host": "pooootato", + "dest-port": "4444", + "disable-copy": "", + "disable-paste": "true", + "enable-audio": "true", + "enable-sftp": "true", + "force-lossless": "true", + "hostname": "potato", + "password": "taste", + "port": "4321", + "read-only": "", + "recording-exclude-mouse": "", + "recording-exclude-output": "", + "recording-include-keys": "", + "recording-name": "heyoooooo", + "recording-path": "/path/to/goo", + "sftp-disable-download": "", + "sftp-disable-upload": "", + "sftp-hostname": "what what good sir", + "sftp-port": "", + "sftp-private-key": "lol i'll never tell", + "sftp-server-alive-interval": "", + "swap-red-blue": "true", + "username": "test", + "wol-send-packet": "", + "wol-udp-port": "", + "wol-wait-time": "" + }, + + // or a numeric identifier - we will probably want to offer a way to allow + // them to specify a path like "ROOT/parent/child" or just "/parent/child" or + // something like that + // TODO: Call the + "parentIdentifier": "ROOT", + "protocol": "vnc" + +} +*/ + + /** + * Convert a provided JSON representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * TODO: Describe disambiguation suffixes, e.g. hostname (parameter), and + * that we will accept without the suffix if it's unambigous. (or not? how about not?) + * + * @param {String} csvData + * The JSON-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseCSV = function parseCSV(csvData) { + + const deferredConnections = $q.defer(); + + return $q.all({ + fieldLookups : getFieldLookups(), + groupLookups : getGroupLookups() + }) + .then(function lookupsReady({fieldLookups, groupLookups}) { + + const {attributes, protocolParameters} = fieldLookups; + + console.log({attributes, protocolParameters}, groupLookups); + + // Convert to an array of arrays, one per CSV row (including the header) + const parsedData = parseCSVData(csvData); + + // Slice off the header row to get the data rows + const connectionData = parsedData.slice(1); + + // Check that the provided CSV is not empty (the parser always + // returns an array) + performBasicChecks(connectionData); + + // The header row - an array of string header values + const header = parsedData[0]; + + // TODO: Connectionify this + deferredConnections.resolve(connectionData); + }); + + return deferredConnections.promise; + + }; + + /** + * Convert a provided JSON representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * @param {String} jsonData + * The JSON-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseJSON = function parseJSON(jsonData) { + + // Parse from JSON into a javascript array + const parsedData = JSON.parse(yamlData); + + // Check that the data is the correct format, and not empty + performBasicChecks(parsedData); + + // Convert to an array of Connection objects and return + const deferredConnections = $q.defer(); + deferredConnections.resolve( + parsedData.map(connection => new Connection(connection))); + return deferredConnections.promise; + + }; + + return service; + +}]); diff --git a/guacamole/src/main/frontend/src/app/settings/styles/buttons.css b/guacamole/src/main/frontend/src/app/settings/styles/buttons.css index f925c5121..167e46a06 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/buttons.css +++ b/guacamole/src/main/frontend/src/app/settings/styles/buttons.css @@ -20,7 +20,8 @@ a.button.add-user, a.button.add-user-group, a.button.add-connection, -a.button.add-connection-group { +a.button.add-connection-group, +a.button.import-connections { font-size: 0.8em; padding-left: 1.8em; position: relative; @@ -29,7 +30,8 @@ a.button.add-connection-group { a.button.add-user::before, a.button.add-user-group::before, a.button.add-connection::before, -a.button.add-connection-group::before { +a.button.add-connection-group::before, +a.button.import-connections::before { content: ' '; position: absolute; @@ -59,3 +61,7 @@ a.button.add-connection::before { a.button.add-connection-group::before { background-image: url('images/action-icons/guac-group-add.svg'); } + +a.button.import-connections::before { + background-image: url('images/action-icons/guac-monitor-add-many.svg'); +} diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html index ca7bc30df..76ebeebec 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html @@ -9,6 +9,10 @@
      + {{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}} + {{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}} diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html new file mode 100644 index 000000000..c9c54e10e --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html @@ -0,0 +1,11 @@ +
      + +
      +

      {{'SETTINGS_CONNECTION_IMPORT.HEADER' | translate}}

      + +
      + + + + +
      diff --git a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js b/guacamole/src/main/frontend/src/app/settings/types/ParseError.js new file mode 100644 index 000000000..b3d3da03a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/settings/types/ParseError.js @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * Service which defines the ParseError class. + */ +angular.module('settings').factory('ParseError', [function defineParseError() { + + /** + * An error representing a parsing failure when attempting to convert + * user-provided data into a list of Connection objects. + * + * @constructor + * @param {ParseError|Object} [template={}] + * The object whose properties should be copied within the new + * ParseError. + */ + var ParseError = function ParseError(template) { + + // Use empty object by default + template = template || {}; + + /** + * A human-readable message describing the error that occurred. + * + * @type String + */ + this.message = template.message; + + /** + * A message which can be translated using the translation service, + * consisting of a translation key and optional set of substitution + * variables. + * + * @type TranslatableMessage + */ + this.translatableMessage = template.translatableMessage; + + }; + + return ParseError; + +}]); diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg b/guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg new file mode 100644 index 000000000..95ab14b76 --- /dev/null +++ b/guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add-many.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 450a455bb..21812d376 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -5,7 +5,7 @@ "APP" : { "NAME" : "Apache Guacamole", - "VERSION" : "${project.version}", + "VERSION" : "1.5.0", "ACTION_ACKNOWLEDGE" : "OK", "ACTION_CANCEL" : "Cancel", @@ -906,6 +906,7 @@ "SETTINGS_CONNECTIONS" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_IMPORT_CONNECTIONS" : "Import", "ACTION_NEW_CONNECTION" : "New Connection", "ACTION_NEW_CONNECTION_GROUP" : "New Group", "ACTION_NEW_SHARING_PROFILE" : "New Sharing Profile", @@ -922,6 +923,13 @@ }, + "SETTINGS_CONNECTION_IMPORT": { + + "HEADER": "Connection Import", + + "ERROR_ARRAY_REQUIRED": "The provided file must contain a list of connections" + }, + "SETTINGS_PREFERENCES" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index 29bb8ddd5..dc6ad08cf 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -47,6 +47,22 @@ module.exports = { module: { rules: [ + // NOTE: This is required in order to parse ES2020 language features, + // like the optional chaining and nullish coalescing operators. It + // specifically needs to operate on the node-modules directory since + // Webpack 4 cannot handle such language features. + { + test: /\.js$/i, + use: { + loader: 'babel-loader', + options: { + presets: [ + ['@babel/preset-env'] + ] + } + } + }, + // Automatically extract imported CSS for later reference within separate CSS file { test: /\.css$/i, From fac76ef0cb1a3ff52bd5c03ba4aa632630175c02 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Wed, 1 Feb 2023 23:25:12 +0000 Subject: [PATCH 148/289] GUACAMOLE-926: Migrate import code to a dedicated module. --- .../importConnectionsController.js | 10 +- .../frontend/src/app/import/importModule.js | 24 ++++ .../import/services/connectionCSVService.js | 103 ++++++++++++++++++ .../services/connectionParseService.js} | 62 +++++------ .../templates/connectionImport.html} | 0 .../{settings => import}/types/ParseError.js | 2 +- .../src/app/index/config/indexRouteConfig.js | 4 +- .../app/rest/services/connectionService.js | 37 +++---- .../src/app/rest/types/DirectoryPatch.js | 95 ++++++++++++++++ .../app/rest/types/DirectoryPatchOutcome.js | 83 ++++++++++++++ 10 files changed, 361 insertions(+), 59 deletions(-) rename guacamole/src/main/frontend/src/app/{settings => import}/controllers/importConnectionsController.js (83%) create mode 100644 guacamole/src/main/frontend/src/app/import/importModule.js create mode 100644 guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js rename guacamole/src/main/frontend/src/app/{settings/services/connectionImportParseService.js => import/services/connectionParseService.js} (98%) rename guacamole/src/main/frontend/src/app/{settings/templates/settingsImport.html => import/templates/connectionImport.html} (100%) rename guacamole/src/main/frontend/src/app/{settings => import}/types/ParseError.js (95%) create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js similarity index 83% rename from guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js rename to guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 33b8914ad..178d6c69d 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -20,11 +20,11 @@ /** * The controller for the connection import page. */ -angular.module('settings').controller('importConnectionsController', ['$scope', '$injector', +angular.module('import').controller('importConnectionsController', ['$scope', '$injector', function importConnectionsController($scope, $injector) { // Required services - const connectionImportParseService = $injector.get('connectionImportParseService'); + const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); function processData(type, data) { @@ -36,18 +36,18 @@ angular.module('settings').controller('importConnectionsController', ['$scope', case "application/json": case "text/json": - requestBody = connectionImportParseService.parseJSON(data); + requestBody = connectionParseService.parseJSON(data); break; case "text/csv": - requestBody = connectionImportParseService.parseCSV(data); + requestBody = connectionParseService.parseCSV(data); break; case "application/yaml": case "application/x-yaml": case "text/yaml": case "text/x-yaml": - requestBody = connectionImportParseService.parseYAML(data); + requestBody = connectionParseService.parseYAML(data); break; } diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js new file mode 100644 index 000000000..6480d62fc --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * The module for code supporting importing user-supplied files. Currently, only + * connection import is supported. + */ +angular.module('import', ['rest']); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js new file mode 100644 index 000000000..361a15283 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -0,0 +1,103 @@ +/* + * 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. + */ + +/* global _ */ + +/** + * A service for parsing user-provided CSV connection data for bulk import. + */ +angular.module('import').factory('connectionCSVService', + ['$injector', function connectionCSVService($injector) { + + // Required services + const $q = $injector.get('$q'); + const $routeParams = $injector.get('$routeParams'); + const schemaService = $injector.get('schemaService'); + + const service = {}; + + /** + * Returns a promise that resolves to an object detailing the connection + * attributes for the current data source, as well as the connection + * paremeters for every protocol, for the current data source. + * + * The object that the promise will contain an "attributes" key that maps to + * a set of attribute names, and a "protocolParameters" key that maps to an + * object mapping protocol names to sets of parameter names for that protocol. + * + * The intended use case for this object is to determine if there is a + * connection parameter or attribute with a given name, by e.g. checking the + * path `.protocolParameters[protocolName]` to see if a protocol exists, + * checking the path `.protocolParameters[protocolName][fieldName]` to see + * if a parameter exists for a given protocol, or checking the path + * `.attributes[fieldName]` to check if a connection attribute exists. + * + * @returns {Promise.} + */ + function getFieldLookups() { + + // The current data source - the one that the connections will be + // imported into + const dataSource = $routeParams.dataSource; + + // Fetch connection attributes and protocols for the current data source + return $q.all({ + attributes : schemaService.getConnectionAttributes(dataSource), + protocols : schemaService.getProtocols(dataSource) + }) + .then(function connectionStructureRetrieved({attributes, protocols}) { + + return { + + // Translate the forms and fields into a flat map of attribute + // name to `true` boolean value + attributes: attributes.reduce( + (attributeMap, form) => { + form.fields.forEach( + field => attributeMap[field.name] = true); + return attributeMap + }, {}), + + // Translate the protocol definitions into a map of protocol + // name to map of field name to `true` boolean value + protocolParameters: _.mapValues( + protocols, protocol => protocol.connectionForms.reduce( + (protocolFieldMap, form) => { + form.fields.forEach( + field => protocolFieldMap[field.name] = true); + return protocolFieldMap; + }, {})) + }; + }); + } + + /** + * + * + * @returns {Promise.>} + * A promise that will resolve to a function that translates a CSV data + * row (array of strings) to a connection object. + */ + service.getCSVTransformer = function getCSVTransformer(headerRow) { + + }; + + return service; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js similarity index 98% rename from guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js rename to guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index f2735acd6..1051b800b 100644 --- a/guacamole/src/main/frontend/src/app/settings/services/connectionImportParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -26,8 +26,8 @@ import { parse as parseYAMLData } from 'yaml' * A service for parsing user-provided JSON, YAML, or JSON connection data into * an appropriate format for bulk uploading using the PATCH REST endpoint. */ -angular.module('settings').factory('connectionImportParseService', - ['$injector', function connectionImportParseService($injector) { +angular.module('import').factory('connectionParseService', + ['$injector', function connectionParseService($injector) { // Required types const Connection = $injector.get('Connection'); @@ -71,35 +71,6 @@ angular.module('settings').factory('connectionImportParseService', }) }); } - - /** - * Convert a provided YAML representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. - * - * @param {String} yamlData - * The YAML-encoded connection list to convert to a PATCH request body. - * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. - */ - service.parseYAML = function parseYAML(yamlData) { - - // Parse from YAML into a javascript array - const parsedData = parseYAMLData(yamlData); - - // Check that the data is the correct format, and not empty - performBasicChecks(parsedData); - - // Convert to an array of Connection objects and return - const deferredConnections = $q.defer(); - deferredConnections.resolve( - parsedData.map(connection => new Connection(connection))); - return deferredConnections.promise; - - }; /** * Returns a promise that resolves to an object detailing the connection @@ -321,6 +292,35 @@ angular.module('settings').factory('connectionImportParseService', }; + /** + * Convert a provided YAML representation of a connection list into a JSON + * string to be submitted to the PATCH REST endpoint. The returned JSON + * string will contain a PATCH operation to create each connection in the + * provided list. + * + * @param {String} yamlData + * The YAML-encoded connection list to convert to a PATCH request body. + * + * @return {Promise.} + * A promise resolving to an array of Connection objects, one for each + * connection in the provided CSV. + */ + service.parseYAML = function parseYAML(yamlData) { + + // Parse from YAML into a javascript array + const parsedData = parseYAMLData(yamlData); + + // Check that the data is the correct format, and not empty + performBasicChecks(parsedData); + + // Convert to an array of Connection objects and return + const deferredConnections = $q.defer(); + deferredConnections.resolve( + parsedData.map(connection => new Connection(connection))); + return deferredConnections.promise; + + }; + /** * Convert a provided JSON representation of a connection list into a JSON * string to be submitted to the PATCH REST endpoint. The returned JSON diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/templates/settingsImport.html rename to guacamole/src/main/frontend/src/app/import/templates/connectionImport.html diff --git a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js similarity index 95% rename from guacamole/src/main/frontend/src/app/settings/types/ParseError.js rename to guacamole/src/main/frontend/src/app/import/types/ParseError.js index b3d3da03a..21cd545d7 100644 --- a/guacamole/src/main/frontend/src/app/settings/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -20,7 +20,7 @@ /** * Service which defines the ParseError class. */ -angular.module('settings').factory('ParseError', [function defineParseError() { +angular.module('import').factory('ParseError', [function defineParseError() { /** * An error representing a parsing failure when attempting to convert diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index 51c423932..dd9cc0637 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -127,10 +127,10 @@ angular.module('index').config(['$routeProvider', '$locationProvider', }) // Connection import page - .when('/settings/:dataSource/import', { + .when('/import/:dataSource/connection', { title : 'APP.NAME', bodyClassName : 'settings', - templateUrl : 'app/settings/templates/settingsImport.html', + templateUrl : 'app/import/templates/connectionImport.html', controller : 'importConnectionsController', resolve : { updateCurrentToken: updateCurrentToken } }) diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index a91c8d05d..053559530 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -156,41 +156,38 @@ angular.module('rest').factory('connectionService', ['$injector', }; /** - * Makes a request to the REST API to create multiple connections, returning a - * a promise that can be used for processing the results of the call. This - * operation is atomic - if any errors are encountered during the connection - * creation process, the entire request will fail, and no connections will be - * created. + * Makes a request to the REST API to apply a supplied list of connection + * patches, returning a promise that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. * - * @param {Connection[]} connections The connections to create. + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. * * @returns {Promise} * A promise for the HTTP call which will succeed if and only if the - * create operation is successful. + * patch operation is successful. */ - service.createConnections = function createConnections(dataSource, connections) { + service.patchConnections = function patchConnections(dataSource, patches) { - // An object containing a PATCH operation to create each connection - const patchBody = connections.map(connection => ({ - op: "add", - path: "/", - value: connection - })); - - // Make a PATCH request to create the connections + // Make the PATCH request return authenticationService.request({ method : 'PATCH', url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', - data : patchBody + data : patches }) // Clear the cache - .then(function connectionUpdated(){ + .then(function connectionsPatched(){ cacheService.connections.removeAll(); - // Clear users cache to force reload of permissions for this - // newly updated connection + // Clear users cache to force reload of permissions for any + // newly created or replaced connections cacheService.users.removeAll(); + }); } diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js new file mode 100644 index 000000000..96a2cb093 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js @@ -0,0 +1,95 @@ +/* + * 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. + */ + +/** + * Service which defines the DirectoryPatch class. + */ +angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch() { + + /** + * The object consumed by REST API calls when representing changes to an + * arbitrary set of directory-based objects. + * @constructor + * + * @template DirectoryObject + * The directory-based object type that this DirectoryPatch will + * operate on. + * + * @param {DirectoryObject|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatch. + */ + var DirectoryPatch = function DirectoryPatch(template) { + + // Use empty object by default + template = template || {}; + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + * + * @type {String} + */ + this.op = template.op; + + /** + * The path of the objects to modify. For creation of new objects, this + * should be "/". Otherwise, it should be "/{identifier}", specifying + * the identifier of the existing object being modified. + * + * @type {String} + * @default '/' + */ + this.path = template.path || '/'; + + /** + * The object being added or replaced, or the identifier of the object + * being removed. + * + * @type {DirectoryObject|String} + */ + this.value = template.value; + + }; + + /** + * All valid patch operations for directory-based objects. + */ + DirectoryPatch.Operation = { + + /** + * Adds the specified object to the relation. + */ + ADD : "add", + + /** + * Removes the specified object from the relation. + */ + REPLACE : "replace", + + /** + * Removes the specified object from the relation. + */ + REMOVE : "remove" + + }; + + return DirectoryPatch; + +}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js new file mode 100644 index 000000000..4534dae13 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js @@ -0,0 +1,83 @@ +/* + * 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. + */ + +/** + * Service which defines the DirectoryPatchOutcome class. + */ +angular.module('rest').factory('DirectoryPatchOutcome', [ + function defineDirectoryPatchOutcome() { + + /** + * An object returned by a PATCH request to a directory REST API, + * representing the outcome associated with a particular patch in the + * request. This object can indicate either a successful or unsuccessful + * response. The error field is only meaningful for unsuccessful patches. + * @constructor + * + * @template DirectoryObject + * The directory-based object type that this DirectoryPatchOutcome + * represents a patch outcome for. + * + * @param {DirectoryObject|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatchOutcome. + */ + var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { + + // Use empty object by default + template = template || {}; + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + * + * @type {String} + */ + this.op = template.op; + + /** + * The path of the object operated on by the corresponding patch in the + * request. + * + * @type {String} + */ + this.path = template.path; + + /** + * The identifier of the object operated on by the corresponding patch + * in the request. If the object was newly created and the PATCH request + * did not fail, this will be the identifier of the newly created object. + * + * @type {String} + */ + this.identifier = template.identifier; + + /** + * The error message associated with the failure, if the patch failed to + * apply. + * + * @type {String} + */ + this.error = template.error; + + }; + + return DirectoryPatch; + +}]); From a6af634d868b11bff7da7c82dccf4ff5cece35ef Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 2 Feb 2023 01:56:33 +0000 Subject: [PATCH 149/289] GUACAMOLE-926: Implement logic for translating CSV rows to connection objects. --- .../importConnectionsController.js | 57 +++- .../import/services/connectionCSVService.js | 258 +++++++++++++++++- .../import/services/connectionParseService.js | 211 ++++---------- .../frontend/src/app/import/styles/import.css | 22 ++ .../import/templates/connectionImport.html | 14 +- .../src/app/import/types/ParseError.js | 19 +- .../frontend/src/app/index/indexModule.js | 1 + .../templates/settingsConnections.html | 2 +- .../main/frontend/src/translations/en.json | 32 ++- 9 files changed, 431 insertions(+), 185 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/styles/import.css diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 178d6c69d..73164a08c 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -25,42 +25,81 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Required services const connectionParseService = $injector.get('connectionParseService'); - const connectionService = $injector.get('connectionService'); + const connectionService = $injector.get('connectionService'); + + // Required types + const ParseError = $injector.get('ParseError'); + const TranslatableMessage = $injector.get('TranslatableMessage'); + + function handleSuccess(data) { + console.log("OMG SUCCESS: ", data) + } + + // Set any caught error message to the scope for display + const handleError = error => { + console.error(error); + $scope.error = error; + } + + // Clear the current error + const clearError = () => delete $scope.error; function processData(type, data) { - - let requestBody; + + // The function that will process all the raw data and return a list of + // patches to be submitted to the API + let processDataCallback; // Parse the data based on the provided mimetype switch(type) { case "application/json": case "text/json": - requestBody = connectionParseService.parseJSON(data); + processDataCallback = connectionParseService.parseJSON; break; case "text/csv": - requestBody = connectionParseService.parseCSV(data); + processDataCallback = connectionParseService.parseCSV; break; case "application/yaml": case "application/x-yaml": case "text/yaml": case "text/x-yaml": - requestBody = connectionParseService.parseYAML(data); + processDataCallback = connectionParseService.parseYAML; break; - + + default: + handleError(new ParseError({ + message: 'Invalid file type: ' + type, + key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', + variables: { TYPE: type } + })); + return; } + + // Make the call to process the data into a series of patches + processDataCallback(data) - console.log(requestBody); + // Send the data off to be imported if parsing is successful + .then(handleSuccess) + + // Display any error found while parsing the file + .catch(handleError); } $scope.upload = function() { + + // Clear any error message from the previous upload attempt + clearError(); const files = angular.element('#file')[0].files; if (files.length <= 0) { - console.error("TODO: This should be a proper error tho"); + handleError(new ParseError({ + message: 'No file supplied', + key: 'CONNECTION_IMPORT.ERROR_NO_FILE_SUPPLIED' + })); return; } diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index 361a15283..ee14f809b 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -19,16 +19,26 @@ /* global _ */ +// A suffix that indicates that a particular header refers to a parameter +const PARAMETER_SUFFIX = ' (parameter)'; + +// A suffix that indicates that a particular header refers to an attribute +const ATTRIBUTE_SUFFIX = ' (attribute)'; + /** * A service for parsing user-provided CSV connection data for bulk import. */ angular.module('import').factory('connectionCSVService', ['$injector', function connectionCSVService($injector) { + + // Required types + const ParseError = $injector.get('ParseError'); + const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services - const $q = $injector.get('$q'); - const $routeParams = $injector.get('$routeParams'); - const schemaService = $injector.get('schemaService'); + const $q = $injector.get('$q'); + const $routeParams = $injector.get('$routeParams'); + const schemaService = $injector.get('schemaService'); const service = {}; @@ -88,14 +98,254 @@ angular.module('import').factory('connectionCSVService', } /** + * Given a CSV header row, create and return a promise that will resolve to + * a function that can take a CSV data row and return a connection object. + * If an error occurs while parsing a particular row, the resolved function + * will throw a ParseError describing the failure. * + * The provided CSV must contain columns for name and protocol. Optionally, + * the parentIdentifier of the target parent connection group, or a connection + * name path e.g. "ROOT/parent/child" may be included. Additionallty, + * connection parameters or attributes can be included. + * + * The names of connection attributes and parameters are not guaranteed to + * be mutually exclusive, so the CSV import format supports a distinguishing + * suffix. A column may be explicitly declared to be a parameter using a + * " (parameter)" suffix, or an attribute using an " (attribute)" suffix. + * No suffix is required if the name is unique across connections and + * attributes. + * + * If a parameter or attribute name conflicts with the standard + * "name", "protocol", "group", or "parentIdentifier" fields, the suffix is + * required. + * + * This returned object will be very similar to the Connection type, with + * the exception that a human-readable "group" field may be present. + * + * If a failure occurs while attempting to create the transformer function, + * the promise will be rejected with a ParseError describing the failure. * * @returns {Promise.>} * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a connection object. + * row (array of strings) to a connection object. */ service.getCSVTransformer = function getCSVTransformer(headerRow) { + // A promise that will be resolved with the transformer or rejected if + // an error occurs + const deferred = $q.defer(); + + getFieldLookups().then(({attributes, protocolParameters}) => { + + // All configuration required to generate a function that can + // transform a row of CSV into a connection object. + // NOTE: This is a single object instead of a collection of variables + // to ensure that no stale references are used - e.g. when one getter + // invokes another getter + const transformConfig = { + + // Callbacks for required fields + nameGetter: undefined, + protocolGetter: undefined, + + // Callbacks for a parent group ID or group path + groupGetter: _.noop, + parentIdentifierGetter: _.noop, + + // Callbacks that will generate either connection attributes or + // parameters. These callbacks will return a {type, name, value} + // object containing the type ("parameter" or "attribute"), + // the name of the attribute or parameter, and the corresponding + // value. + parameterOrAttributeGetters: [] + + }; + + // A set of all headers that have been seen so far. If any of these + // are duplicated, the CSV is invalid. + const headerSet = {}; + + // Iterate through the headers one by one + headerRow.forEach((rawHeader, index) => { + + // Trim to normalize all headers + const header = rawHeader.trim(); + + // Check if the header is duplicated + if (headerSet[header]) { + deferred.reject(new ParseError({ + message: 'Duplicate CSV Header: ' + header, + translatableMessage: new TranslatableMessage({ + key: 'CONNECTION_IMPORT.ERROR_DUPLICATE_CSV_HEADER', + variables: { HEADER: header } + }) + })); + return; + } + + // Mark that this particular header has already been seen + headerSet[header] = true; + + // A callback that returns the field at the current index + const fetchFieldAtIndex = row => row[index]; + + // Set up the name callback + if (header == 'name') + transformConfig.nameGetter = fetchFieldAtIndex; + + // Set up the protocol callback + else if (header == 'protocol') + transformConfig.protocolGetter = fetchFieldAtIndex; + + // Set up the group callback + else if (header == 'group') + transformConfig.groupGetter = fetchFieldAtIndex; + + // Set up the group parent ID callback + else if (header == 'parentIdentifier') + transformConfig.parentIdentifierGetter = fetchFieldAtIndex; + + // At this point, any other header might refer to a connection + // parameter or to an attribute + + // A field may be explicitly specified as a parameter + else if (header.endsWith(PARAMETER_SUFFIX)) { + + // Push as an explicit parameter getter + const parameterName = header.replace(PARAMETER_SUFFIX); + transformConfig.parameterOrAttributeGetters.push( + row => ({ + type: 'parameter', + name: parameterName, + value: fetchFieldAtIndex(row) + }) + ); + } + + // A field may be explicitly specified as a parameter + else if (header.endsWith(ATTRIBUTE_SUFFIX)) { + + // Push as an explicit attribute getter + const attributeName = header.replace(ATTRIBUTE_SUFFIX); + transformConfig.parameterOrAttributeGetters.push( + row => ({ + type: 'attribute', + name: parameterName, + value: fetchFieldAtIndex(row) + }) + ); + } + + // The field is ambiguous, either an attribute or parameter, + // so the getter will have to determine this for every row + else + transformConfig.parameterOrAttributeGetters.push(row => { + + // The name is just the value of the current header + const name = header; + + // The value is at the index that matches the position + // of the header + const value = fetchFieldAtIndex(row); + + // The protocol may determine whether a field is + // a parameter or an attribute (or both) + const protocol = transformConfig.protocolGetter(row); + + // Determine if the field refers to an attribute or a + // parameter (or both, which is an error) + const isAttribute = !!attributes[name]; + const isParameter = !!_.get( + protocolParameters, [protocol, name]); + + // If there is both an attribute and a protocol-specific + // parameter with the provided name, it's impossible to + // figure out which this should be + if (isAttribute && isParameter) + throw new ParseError({ + message: 'Ambiguous CSV Header: ' + header, + key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', + variables: { HEADER: header } + }); + + // It's neither an attribute or a parameter + else if (!isAttribute && !isParameter) + throw new ParseError({ + message: 'Invalid CSV Header: ' + header, + key: 'CONNECTION_IMPORT.ERROR_INVALID_CSV_HEADER', + variables: { HEADER: header } + }); + + // Choose the appropriate type + const type = isAttribute ? 'attributes' : 'parameters'; + + return { type, name, value }; + }); + }); + + // Fail if the name wasn't provided + if (!transformConfig.nameGetter) + return deferred.reject(new ParseError({ + message: 'The connection name must be provided', + key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' + })); + + // Fail if the protocol wasn't provided + if (!transformConfig.protocolGetter) + return deferred.reject(new ParseError({ + message: 'The connection protocol must be provided', + key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' + })); + + // The function to transform a CSV row into a connection object + deferred.resolve(function transformCSVRow(row) { + + const { + nameGetter, protocolGetter, + parentIdentifierGetter, groupGetter, + parameterOrAttributeGetters + } = transformConfig; + + // Set name and protocol + const name = nameGetter(row); + const protocol = protocolGetter(row); + + // Set the parent group ID and/or group path + const group = groupGetter && groupGetter(row); + const parentIdentifier = ( + parentIdentifierGetter && parentIdentifierGetter(row)); + + return { + + // Simple fields that are not protocol-specific + ...{ + name, + protocol, + parentIdentifier, + group + }, + + // Fields that might potentially be either attributes or + // parameters, depending on the protocol + ...parameterOrAttributeGetters.reduce((values, getter) => { + + // Determine the type, name, and value + const { type, name, value } = getter(row); + + // Set the value and continue on to the next attribute + // or parameter + values[type][name] = value; + return values; + + }, {parameters: {}, attributes: {}}) + + } + + }); + + }); + + return deferred.promise; }; return service; diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 1051b800b..82b7e0c6b 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -38,15 +38,17 @@ angular.module('import').factory('connectionParseService', const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); const schemaService = $injector.get('schemaService'); + const connectionCSVService = $injector.get('connectionCSVService'); const connectionGroupService = $injector.get('connectionGroupService'); const service = {}; /** * Perform basic checks, common to all file types - namely that the parsed - * data is an array, and contains at least one connection entry. + * data is an array, and contains at least one connection entry. Returns an + * error if any of these basic checks fails. * - * @throws {ParseError} + * returns {ParseError} * An error describing the parsing failure, if one of the basic checks * fails. */ @@ -54,80 +56,20 @@ angular.module('import').factory('connectionParseService', // Make sure that the file data parses to an array (connection list) if (!(parsedData instanceof Array)) - throw new ParseError({ + return new ParseError({ message: 'Import data must be a list of connections', - translatableMessage: new TranslatableMessage({ - key: 'SETTINGS_CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' - }) + key: 'CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' }); // Make sure that the connection list is not empty - contains at least // one connection if (!parsedData.length) - throw new ParseError({ - message: 'The provided CSV file is empty', - translatableMessage: new TranslatableMessage({ - key: 'SETTINGS_CONNECTION_IMPORT.ERROR_EMPTY_FILE' - }) + return new ParseError({ + message: 'The provided file is empty', + key: 'CONNECTION_IMPORT.ERROR_EMPTY_FILE' }); } - /** - * Returns a promise that resolves to an object detailing the connection - * attributes for the current data source, as well as the connection - * paremeters for every protocol, for the current data source. - * - * The object that the promise will contain an "attributes" key that maps to - * a set of attribute names, and a "protocolParameters" key that maps to an - * object mapping protocol names to sets of parameter names for that protocol. - * - * The intended use case for this object is to determine if there is a - * connection parameter or attribute with a given name, by e.g. checking the - * path `.protocolParameters[protocolName]` to see if a protocol exists, - * checking the path `.protocolParameters[protocolName][fieldName]` to see - * if a parameter exists for a given protocol, or checking the path - * `.attributes[fieldName]` to check if a connection attribute exists. - * - * @returns {Promise.} - */ - function getFieldLookups() { - - // The current data source - the one that the connections will be - // imported into - const dataSource = $routeParams.dataSource; - - // Fetch connection attributes and protocols for the current data source - return $q.all({ - attributes : schemaService.getConnectionAttributes(dataSource), - protocols : schemaService.getProtocols(dataSource) - }) - .then(function connectionStructureRetrieved({attributes, protocols}) { - - return { - - // Translate the forms and fields into a flat map of attribute - // name to `true` boolean value - attributes: attributes.reduce( - (attributeMap, form) => { - form.fields.forEach( - field => attributeMap[field.name] = true); - return attributeMap - }, {}), - - // Translate the protocol definitions into a map of protocol - // name to map of field name to `true` boolean value - protocolParameters: _.mapValues( - protocols, protocol => protocol.connectionForms.reduce( - (protocolFieldMap, form) => { - form.fields.forEach( - field => protocolFieldMap[field.name] = true); - return protocolFieldMap; - }, {})) - }; - }); - - } - /** * Returns a promise that resolves to an object mapping potential groups @@ -181,68 +123,9 @@ angular.module('import').factory('connectionParseService', return deferredGroupLookups.promise; } - -/* -// Example Connection JSON -{ - "attributes": { - "failover-only": "true", - "guacd-encryption": "none", - "guacd-hostname": "potato", - "guacd-port": "1234", - "ksm-user-config-enabled": "true", - "max-connections": "1", - "max-connections-per-user": "1", - "weight": "1" - }, - "name": "Bloatato", - "parameters": { - "audio-servername": "heyoooooooo", - "clipboard-encoding": "", - "color-depth": "", - "create-recording-path": "", - "cursor": "remote", - "dest-host": "pooootato", - "dest-port": "4444", - "disable-copy": "", - "disable-paste": "true", - "enable-audio": "true", - "enable-sftp": "true", - "force-lossless": "true", - "hostname": "potato", - "password": "taste", - "port": "4321", - "read-only": "", - "recording-exclude-mouse": "", - "recording-exclude-output": "", - "recording-include-keys": "", - "recording-name": "heyoooooo", - "recording-path": "/path/to/goo", - "sftp-disable-download": "", - "sftp-disable-upload": "", - "sftp-hostname": "what what good sir", - "sftp-port": "", - "sftp-private-key": "lol i'll never tell", - "sftp-server-alive-interval": "", - "swap-red-blue": "true", - "username": "test", - "wol-send-packet": "", - "wol-udp-port": "", - "wol-wait-time": "" - }, - - // or a numeric identifier - we will probably want to offer a way to allow - // them to specify a path like "ROOT/parent/child" or just "/parent/child" or - // something like that - // TODO: Call the - "parentIdentifier": "ROOT", - "protocol": "vnc" - -} -*/ /** - * Convert a provided JSON representation of a connection list into a JSON + * Convert a provided CSV representation of a connection list into a JSON * string to be submitted to the PATCH REST endpoint. The returned JSON * string will contain a PATCH operation to create each connection in the * provided list. @@ -259,36 +142,44 @@ angular.module('import').factory('connectionParseService', */ service.parseCSV = function parseCSV(csvData) { - const deferredConnections = $q.defer(); + // Convert to an array of arrays, one per CSV row (including the header) + // NOTE: skip_empty_lines is required, or a trailing newline will error + let parsedData; + try { + parsedData = parseCSVData(csvData, {skip_empty_lines: true}); + } - return $q.all({ - fieldLookups : getFieldLookups(), - groupLookups : getGroupLookups() - }) - .then(function lookupsReady({fieldLookups, groupLookups}) { + // If the CSV parser throws an error, reject with that error. No + // translation key will be available here. + catch(error) { + console.error(error); + const deferred = $q.defer(); + deferred.reject(error); + return deferred.promise; + } + + // Slice off the header row to get the data rows + const connectionData = parsedData.slice(1); + + // Check that the provided CSV is not empty (the parser always + // returns an array) + const checkError = performBasicChecks(connectionData); + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } - const {attributes, protocolParameters} = fieldLookups; - - console.log({attributes, protocolParameters}, groupLookups); - - // Convert to an array of arrays, one per CSV row (including the header) - const parsedData = parseCSVData(csvData); - - // Slice off the header row to get the data rows - const connectionData = parsedData.slice(1); - - // Check that the provided CSV is not empty (the parser always - // returns an array) - performBasicChecks(connectionData); - - // The header row - an array of string header values - const header = parsedData[0]; - - // TODO: Connectionify this - deferredConnections.resolve(connectionData); - }); + // The header row - an array of string header values + const header = parsedData[0]; - return deferredConnections.promise; + return connectionCSVService.getCSVTransformer(header).then( + + // If the transformer was successfully generated, apply it to the + // data rows + // TODO: Also apply the group -> parentIdentifier transform + csvTransformer => connectionData.map(csvTransformer) + ); }; @@ -303,7 +194,7 @@ angular.module('import').factory('connectionParseService', * * @return {Promise.} * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. + * connection in the provided YAML. */ service.parseYAML = function parseYAML(yamlData) { @@ -311,7 +202,9 @@ angular.module('import').factory('connectionParseService', const parsedData = parseYAMLData(yamlData); // Check that the data is the correct format, and not empty - performBasicChecks(parsedData); + const checkError = performBasicChecks(connectionData); + if (checkError) + return $q.defer().reject(checkError); // Convert to an array of Connection objects and return const deferredConnections = $q.defer(); @@ -332,7 +225,7 @@ angular.module('import').factory('connectionParseService', * * @return {Promise.} * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. + * connection in the provided JSON. */ service.parseJSON = function parseJSON(jsonData) { @@ -340,7 +233,9 @@ angular.module('import').factory('connectionParseService', const parsedData = JSON.parse(yamlData); // Check that the data is the correct format, and not empty - performBasicChecks(parsedData); + const checkError = performBasicChecks(connectionData); + if (checkError) + return $q.defer().reject(checkError); // Convert to an array of Connection objects and return const deferredConnections = $q.defer(); diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css new file mode 100644 index 000000000..f5551bb42 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -0,0 +1,22 @@ +/* + * 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. + */ + +.import .parseError { + color: red; +} \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index c9c54e10e..39b12fece 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -1,11 +1,23 @@
      -

      {{'SETTINGS_CONNECTION_IMPORT.HEADER' | translate}}

      +

      {{'CONNECTION_IMPORT.HEADER' | translate}}

      + + +

      + + +

      + {{error.message}} +

      +
      diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js index 21cd545d7..1e4ce478e 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -44,13 +44,22 @@ angular.module('import').factory('ParseError', [function defineParseError() { this.message = template.message; /** - * A message which can be translated using the translation service, - * consisting of a translation key and optional set of substitution - * variables. + * The key associated with the translation string that used when + * displaying this message. * - * @type TranslatableMessage + * @type String */ - this.translatableMessage = template.translatableMessage; + this.key = template.key; + + /** + * The object which should be passed through to the translation service + * for the sake of variable substitution. Each property of the provided + * object will be substituted for the variable of the same name within + * the translation string. + * + * @type Object + */ + this.variables = template.variables; }; diff --git a/guacamole/src/main/frontend/src/app/index/indexModule.js b/guacamole/src/main/frontend/src/app/index/indexModule.js index e0281f439..2bcc94527 100644 --- a/guacamole/src/main/frontend/src/app/index/indexModule.js +++ b/guacamole/src/main/frontend/src/app/index/indexModule.js @@ -35,6 +35,7 @@ angular.module('index', [ 'client', 'clipboard', 'home', + 'import', 'login', 'manage', 'navigation', diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html index 76ebeebec..0718307a1 100644 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html +++ b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html @@ -11,7 +11,7 @@ {{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}} + href="#/import/{{dataSource | escape}}/connection/">{{'SETTINGS_CONNECTIONS.ACTION_IMPORT_CONNECTIONS' | translate}} Date: Sat, 4 Feb 2023 02:05:17 +0000 Subject: [PATCH 150/289] GUACAMOLE-926: Translate to API patches in patch service. --- .../import/services/connectionParseService.js | 146 +++++++++++++++--- .../main/frontend/src/translations/en.json | 5 +- 2 files changed, 124 insertions(+), 27 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 82b7e0c6b..322e22c15 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -31,6 +31,7 @@ angular.module('import').factory('connectionParseService', // Required types const Connection = $injector.get('Connection'); + const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); const TranslatableMessage = $injector.get('TranslatableMessage'); @@ -124,6 +125,62 @@ angular.module('import').factory('connectionParseService', return deferredGroupLookups.promise; } + /** + * Returns a promise that will resolve to a transformer function that will + * take an object that may a "group" field, replacing it if present with a + * "parentIdentifier". If both a "group" and "parentIdentifier" field are + * present on the provided object, or if no group exists at the specified + * path, the function will throw a ParseError describing the failure. + * + * @returns {Promise.>} + * A promise that will resolve to a function that will transform a + * "group" field into a "parentIdentifier" field if possible. + */ + function getGroupTransformer() { + return getGroupLookups().then(lookups => connection => { + + // If there's no group to translate, do nothing + if (!connection.group) + return; + + // If both are specified, the parent group is ambigious + if (connection.parentIdentifier) + throw new ParseError({ + message: 'Only one of group or parentIdentifier can be set', + key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + }); + + // Look up the parent identifier for the specified group path + const identifier = lookups[connection.group]; + + // If the group doesn't match anything in the tree + if (!identifier) + throw new ParseError({ + message: 'No group found named: ' + connection.group, + key: 'CONNECTION_IMPORT.ERROR_INVALID_GROUP', + variables: { GROUP: connection.group } + }); + + // Set the parent identifier now that it's known + return { + ...connection, + parentIdentifier: identifier + }; + + }); + } + + // Translate a given javascript object to a full-fledged Connection + const connectionTransformer = connection => new Connection(connection); + + // Translate a Connection object to a patch requesting the creation of said + // Connection + const patchTransformer = connection => new DirectoryPatch({ + op: 'add', + path: '/', + value: connection + }); + /** * Convert a provided CSV representation of a connection list into a JSON * string to be submitted to the PATCH REST endpoint. The returned JSON @@ -172,14 +229,29 @@ angular.module('import').factory('connectionParseService', // The header row - an array of string header values const header = parsedData[0]; - - return connectionCSVService.getCSVTransformer(header).then( - - // If the transformer was successfully generated, apply it to the - // data rows - // TODO: Also apply the group -> parentIdentifier transform - csvTransformer => connectionData.map(csvTransformer) - ); + + return $q.all({ + csvTransformer : connectionCSVService.getCSVTransformer(header), + groupTransformer : getGroupTransformer() + }) + + // Transform the rows from the CSV file to an array of API patches + .then(({csvTransformer, groupTransformer}) => connectionData.map( + dataRow => { + + // Translate the raw CSV data to a javascript object + let connectionObject = csvTransformer(dataRow); + + // Translate the group on the object to a parentIdentifier + connectionObject = groupTransformer(connectionObject); + + // Translate to a full-fledged Connection + const connection = connectionTransformer(connectionObject); + + // Finally, translate to a patch for creating the connection + return patchTransformer(connection); + + })); }; @@ -203,14 +275,26 @@ angular.module('import').factory('connectionParseService', // Check that the data is the correct format, and not empty const checkError = performBasicChecks(connectionData); - if (checkError) - return $q.defer().reject(checkError); - - // Convert to an array of Connection objects and return - const deferredConnections = $q.defer(); - deferredConnections.resolve( - parsedData.map(connection => new Connection(connection))); - return deferredConnections.promise; + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } + + // Transform the data from the YAML file to an array of API patches + return getGroupTransformer().then( + groupTransformer => parsedData.map(connectionObject => { + + // Translate the group on the object to a parentIdentifier + connectionObject = groupTransformer(connectionObject); + + // Translate to a full-fledged Connection + const connection = connectionTransformer(connectionObject); + + // Finally, translate to a patch for creating the connection + return patchTransformer(connection); + + })); }; @@ -231,17 +315,29 @@ angular.module('import').factory('connectionParseService', // Parse from JSON into a javascript array const parsedData = JSON.parse(yamlData); - + // Check that the data is the correct format, and not empty const checkError = performBasicChecks(connectionData); - if (checkError) - return $q.defer().reject(checkError); - - // Convert to an array of Connection objects and return - const deferredConnections = $q.defer(); - deferredConnections.resolve( - parsedData.map(connection => new Connection(connection))); - return deferredConnections.promise; + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } + + // Transform the data from the YAML file to an array of API patches + return getGroupTransformer().then( + groupTransformer => parsedData.map(connectionObject => { + + // Translate the group on the object to a parentIdentifier + connectionObject = groupTransformer(connectionObject); + + // Translate to a full-fledged Connection + const connection = connectionTransformer(connectionObject); + + // Finally, translate to a patch for creating the connection + return patchTransformer(connection); + + })); }; diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 89647a491..98b983b9f 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -197,11 +197,12 @@ "ERROR_EMPTY_FILE": "The provided file is empty", "ERROR_INVALID_CSV_HEADER": "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter", + "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_FILE_TYPE": "Invalid import file type \"{TYPE}\"", "ERROR_NO_FILE_SUPPLIED": "Please select a file to import", - "ERROR_REQUIRED_GROUP": - "Either group or parentIdentifier must be specified, but not both", + "ERROR_AMBIGUOUS_PARENT_GROUP": + "Both group and parentIdentifier may be not specified at the same time", "ERROR_REQUIRED_PROTOCOL": "No connection protocol found in the provided file", "ERROR_REQUIRED_NAME": From 314adf6c235f59feaa117132424118fabe82e0f1 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 7 Feb 2023 00:31:06 +0000 Subject: [PATCH 151/289] GUACAMOLE-926: Remove patch update functionality. It's needed for batch import, and it's a can of worms. --- .../ActiveConnectionService.java | 8 -- .../jdbc/base/DirectoryObjectService.java | 18 ---- .../base/ModeledDirectoryObjectService.java | 14 --- .../guacamole/auth/jdbc/base/ObjectModel.java | 29 +----- .../jdbc/connection/ConnectionDirectory.java | 14 +-- .../auth/jdbc/connection/ConnectionModel.java | 5 + .../jdbc/connection/ConnectionService.java | 7 +- .../connectiongroup/ConnectionGroupModel.java | 6 ++ .../ConnectionGroupService.java | 7 +- .../sharingprofile/SharingProfileModel.java | 6 ++ .../sharingprofile/SharingProfileService.java | 7 +- .../guacamole/auth/jdbc/user/UserService.java | 4 - .../auth/jdbc/usergroup/UserGroupService.java | 4 - .../rest/directory/DirectoryResource.java | 94 ++++--------------- 14 files changed, 39 insertions(+), 184 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java index 80c75dbbb..046cee1e2 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java @@ -166,14 +166,6 @@ public class ActiveConnectionService } - @Override - public void updateExternalObject(ModeledAuthenticatedUser user, ActiveConnection object) throws GuacamoleException { - - // Updating active connections is not implemented - throw new GuacamoleSecurityException("Permission denied."); - - } - /** * Retrieve the permission set for the specified user that relates * to access to active connections. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java index 29d40cc49..590e01e9f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/DirectoryObjectService.java @@ -116,24 +116,6 @@ public interface DirectoryObjectService { void deleteObject(ModeledAuthenticatedUser user, String identifier) throws GuacamoleException; - /** - * Updates the object corresponding to the given external representation, - * applying any changes that have been made. If no such object exists, - * this function has no effect. - * - * @param user - * The user updating the object. - * - * @param object - * The external object to apply updates from. - * - * @throws GuacamoleException - * If the user lacks permission to update the object, or an error - * occurs while updating the object. - */ - void updateExternalObject(ModeledAuthenticatedUser user, ExternalType object) - throws GuacamoleException; - /** * Updates the given object, applying any changes that have been made. If * no such object exists, this function has no effect. diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java index 75c25926b..80bc7eb5c 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledDirectoryObjectService.java @@ -511,20 +511,6 @@ public abstract class ModeledDirectoryObjectService getIdentifiers(ModeledAuthenticatedUser user) throws GuacamoleException { diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java index 69eb1fd36..c3052b1b4 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ObjectModel.java @@ -21,8 +21,6 @@ package org.apache.guacamole.auth.jdbc.base; import java.util.Collection; -import org.apache.guacamole.GuacamoleException; - /** * Object representation of a Guacamole object, such as a user or connection, * as represented in the database. @@ -77,7 +75,7 @@ public abstract class ObjectModel { * * @return * The ID of this object in the database, or null if this object was - * not retrieved from or intended to update the database. + * not retrieved from the database. */ public Integer getObjectID() { return objectID; @@ -93,31 +91,6 @@ public abstract class ObjectModel { this.objectID = objectID; } - /** - * Given a text identifier, attempt to convert to an integer database ID. - * If the identifier is valid, the database ID will be set to this value. - * Otherwise, a GuacamoleException will be thrown. - * - * @param identifier - * The identifier to convert to an integer and set on the database - * model, if valid. - * - * @throws GuacamoleException - * If the provided identifier is not a valid integer. - */ - public void setObjectID(String identifier) throws GuacamoleException { - - // Try to convert the provided identifier to an integer ID - try { - setObjectID(Integer.parseInt(identifier)); - } - - catch (NumberFormatException e) { - throw new GuacamoleException( - "Database identifiers must be integers."); - } - } - /** * Returns a map of attribute name/value pairs for all attributes associated * with this model which do not have explicit mappings to actual model diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java index b2f3e2bea..3e364f509 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionDirectory.java @@ -68,18 +68,8 @@ public class ConnectionDirectory extends JDBCDirectory { @Override @Transactional public void update(Connection object) throws GuacamoleException { - - // If the provided connection is already an internal type, update - // using the internal method - if (object instanceof ModeledConnection) - connectionService.updateObject( - getCurrentUser(), (ModeledConnection) object); - - // If the type is not already the expected internal type, use the - // external update method - else - connectionService.updateExternalObject(getCurrentUser(), object); - + ModeledConnection connection = (ModeledConnection) object; + connectionService.updateObject(getCurrentUser(), connection); } @Override diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java index 03413650b..da454025d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionModel.java @@ -387,4 +387,9 @@ public class ConnectionModel extends ChildObjectModel { } + @Override + public void setIdentifier(String identifier) { + throw new UnsupportedOperationException("Connection identifiers are derived from IDs. They cannot be set."); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java index cdf2afb76..b3ed89ce8 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java @@ -110,19 +110,14 @@ public class ConnectionService extends ModeledChildDirectoryObjectService addedObjects = new ArrayList<>(); - Collection updatedObjects = new ArrayList<>(); Collection removedIdentifiers = new ArrayList<>(); // A list of all responses associated with the successful @@ -527,64 +526,6 @@ public abstract class DirectoryResource updatedIterator = updatedObjects.iterator(); - while (updatedIterator.hasNext()) { - - InternalType internal = updatedIterator.next(); - fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, - internal.getIdentifier(), internal); - - } - // Fire directory success events for each removed object Iterator removedIterator = removedIdentifiers.iterator(); while (removedIterator.hasNext()) { String identifier = removedIterator.next(); fireDirectorySuccessEvent( - DirectoryEvent.Operation.UPDATE, + DirectoryEvent.Operation.REMOVE, identifier, null); } From 761438e02d4a86a4b12269f2494a431860cfa794 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 9 Feb 2023 01:51:14 +0000 Subject: [PATCH 152/289] GUACAMOLE-926: Also accept user and group identifiers to post in a seperate request. --- .../import/services/connectionCSVService.js | 138 ++++++-- .../import/services/connectionParseService.js | 322 +++++++++++------- .../src/app/import/types/ParseError.js | 2 +- .../src/app/import/types/ParseResult.js | 87 +++++ .../src/app/import/types/ProtoConnection.js | 111 ++++++ .../main/frontend/src/translations/en.json | 4 + 6 files changed, 522 insertions(+), 142 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/types/ParseResult.js create mode 100644 guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index ee14f809b..a104cdd46 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -33,6 +33,7 @@ angular.module('import').factory('connectionCSVService', // Required types const ParseError = $injector.get('ParseError'); + const ProtoConnection = $injector.get('ProtoConnection'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -43,7 +44,7 @@ angular.module('import').factory('connectionCSVService', const service = {}; /** - * Returns a promise that resolves to an object detailing the connection + * Returns a promise that resolves to a object detailing the connection * attributes for the current data source, as well as the connection * paremeters for every protocol, for the current data source. * @@ -96,12 +97,66 @@ angular.module('import').factory('connectionCSVService', }; }); } + + /** + * Split a raw user-provided, semicolon-seperated list of identifiers into + * an array of identifiers. If identifiers contain semicolons, they can be + * escaped with backslashes, and backslashes can also be escaped using other + * backslashes. + * + * @param {type} rawIdentifiers + * The raw string value as fetched from the CSV. + * + * @returns {Array.} + * An array of identifier values. + */ + function splitIdentifiers(rawIdentifiers) { + + // Keep track of whether a backslash was seen + let escaped = false; + + return _.reduce(rawIdentifiers, (identifiers, ch) => { + + // The current identifier will be the last one in the final list + let identifier = identifiers[identifiers.length - 1]; + + // If a semicolon is seen, set the "escaped" flag and continue + // to the next character + if (!escaped && ch == '\\') { + escaped = true; + return identifiers; + } + + // End the current identifier and start a new one if there's an + // unescaped semicolon + else if (!escaped && ch == ';') { + identifiers.push(''); + return identifiers; + } + + // In all other cases, just append to the identifier + else { + identifier += ch; + escaped = false; + } + + // Save the updated identifier to the list + identifiers[identifiers.length - 1] = identifier; + + return identifiers; + + }, ['']) + + // Filter out any 0-length (empty) identifiers + .filter(identifier => identifier.length); + + } /** * Given a CSV header row, create and return a promise that will resolve to - * a function that can take a CSV data row and return a connection object. - * If an error occurs while parsing a particular row, the resolved function - * will throw a ParseError describing the failure. + * a function that can take a CSV data row and return a ProtoConnection + * object. If an error occurs while parsing a particular row, the resolved + * function will throw a ParseError describing the failure. * * The provided CSV must contain columns for name and protocol. Optionally, * the parentIdentifier of the target parent connection group, or a connection @@ -120,14 +175,17 @@ angular.module('import').factory('connectionCSVService', * required. * * This returned object will be very similar to the Connection type, with - * the exception that a human-readable "group" field may be present. + * the exception that a human-readable "group" field may be present, in + * addition to "user" and "userGroup" fields containing arrays of user and + * user group identifiers for whom read access should be granted to this + * connection. * * If a failure occurs while attempting to create the transformer function, * the promise will be rejected with a ParseError describing the failure. * * @returns {Promise.>} * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a connection object. + * row (array of strings) to a ProtoConnection object. */ service.getCSVTransformer = function getCSVTransformer(headerRow) { @@ -149,8 +207,12 @@ angular.module('import').factory('connectionCSVService', protocolGetter: undefined, // Callbacks for a parent group ID or group path - groupGetter: _.noop, - parentIdentifierGetter: _.noop, + groupGetter: undefined, + parentIdentifierGetter: undefined, + + // Callbacks for user and user group identifiers + usersGetter: () => [], + userGroupsGetter: () => [], // Callbacks that will generate either connection attributes or // parameters. These callbacks will return a {type, name, value} @@ -188,6 +250,11 @@ angular.module('import').factory('connectionCSVService', // A callback that returns the field at the current index const fetchFieldAtIndex = row => row[index]; + + // A callback that splits identifier lists by semicolon + // characters into a javascript list of identifiers + const identifierListCallback = row => + splitIdentifiers(fetchFieldAtIndex(row)); // Set up the name callback if (header == 'name') @@ -203,7 +270,18 @@ angular.module('import').factory('connectionCSVService', // Set up the group parent ID callback else if (header == 'parentIdentifier') - transformConfig.parentIdentifierGetter = fetchFieldAtIndex; + transformConfig.parentIdentifierGetter = ( + identifierListCallback); + + // Set the user identifiers callback + else if (header == 'users') + transformConfig.usersGetter = ( + identifierListCallback); + + // Set the user group identifiers callback + else if (header == 'groups') + transformConfig.userGroupsGetter = ( + identifierListCallback); // At this point, any other header might refer to a connection // parameter or to an attribute @@ -282,47 +360,61 @@ angular.module('import').factory('connectionCSVService', return { type, name, value }; }); }); + + const { + nameGetter, protocolGetter, + parentIdentifierGetter, groupGetter, + usersGetter, userGroupsGetter, + parameterOrAttributeGetters + } = transformConfig; // Fail if the name wasn't provided - if (!transformConfig.nameGetter) + if (!nameGetter) return deferred.reject(new ParseError({ message: 'The connection name must be provided', key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' })); // Fail if the protocol wasn't provided - if (!transformConfig.protocolGetter) + if (!protocolGetter) return deferred.reject(new ParseError({ message: 'The connection protocol must be provided', key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' })); + + // If both are specified, the parent group is ambigious + if (parentIdentifierGetter && groupGetter) + throw new ParseError({ + message: 'Only one of group or parentIdentifier can be set', + key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + }); // The function to transform a CSV row into a connection object deferred.resolve(function transformCSVRow(row) { - - const { - nameGetter, protocolGetter, - parentIdentifierGetter, groupGetter, - parameterOrAttributeGetters - } = transformConfig; - // Set name and protocol + // Get name and protocol const name = nameGetter(row); const protocol = protocolGetter(row); + + // Get any users or user groups who should be granted access + const users = usersGetter(row); + const groups = userGroupsGetter(row); - // Set the parent group ID and/or group path + // Get the parent group ID and/or group path const group = groupGetter && groupGetter(row); const parentIdentifier = ( parentIdentifierGetter && parentIdentifierGetter(row)); - return { + return new ProtoConnection({ - // Simple fields that are not protocol-specific + // Fields that are not protocol-specific ...{ name, protocol, parentIdentifier, - group + group, + users, + groups }, // Fields that might potentially be either attributes or @@ -339,7 +431,7 @@ angular.module('import').factory('connectionCSVService', }, {parameters: {}, attributes: {}}) - } + }); }); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 322e22c15..f2f6c1daa 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -33,6 +33,7 @@ angular.module('import').factory('connectionParseService', const Connection = $injector.get('Connection'); const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); + const ParseResult = $injector.get('ParseResult'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -41,8 +42,81 @@ angular.module('import').factory('connectionParseService', const schemaService = $injector.get('schemaService'); const connectionCSVService = $injector.get('connectionCSVService'); const connectionGroupService = $injector.get('connectionGroupService'); + const userService = $injector.get('userService'); + const userGroupService = $injector.get('userGroupService'); const service = {}; + + /** + * Resolves to an object whose keys are all valid identifiers in the current + * data source. The provided `requestFunction` should resolve to such an + * object when provided with the current data source. + * + * @param {Function.>} requestFunction + * A function that, given a data source, will return a promise resolving + * to an object with keys that are unique identifiers for entities in + * that data source. + * + * @returns {Promise.>} + * A promise that will resolve to an function that, given an array of + * identifiers, will return all identifiers that do not exist as keys in + * the object returned by `requestFunction`, i.e. all identifiers that + * do not exist in the current data source. + */ + function getIdentifierMap(requestFunction) { + + // The current data source to which all the identifiers will belong + const dataSource = $routeParams.dataSource; + + // Make the request and return the response, which should be an object + // whose keys are all valid identifiers for the current data source + return requestFunction(dataSource); + } + + /** + * Return a promise that resolves to a function that takes an array of + * identifiers, returning a ParseError describing the invalid identifiers + * from the list. The provided `requestFunction` should resolve to such an + * object whose keys are all valid identifiers when provided with the + * current data source. + * + * @param {Function.>} requestFunction + * A function that, given a data source, will return a promise resolving + * to an object with keys that are unique identifiers for entities in + * that data source. + * + * @returns {Promise.>} + * A promise that will resolve to a function to check the validity of + * each identifier in a provided array, returning a ParseError + * describing the problem if any are not valid. + */ + function getInvalidIdentifierErrorChecker(requestFunction) { + + // Fetch all the valid user identifiers in the system, and + return getIdentifierMap(requestFunction).then(validIdentifiers => + + // The resolved function that takes a list of user group identifiers + allIdentifiers => { + + // Filter to only include invalid identifiers + const invalidIdentifiers = _.filter(allIdentifiers, + identifier => !validIdentifiers[identifier]); + + if (invalidIdentifiers.length) { + + // Quote and comma-seperate for display + const identifierList = invalidIdentifiers.map( + identifier => '"' + identifier + '"').join(', '); + + return new ParseError({ + message: 'Invalid User Group Identifiers: ' + identifierList, + key: 'CONNECTION_IMPORT.ERROR_INVALID_USER_GROUP_IDENTIFIERS', + variables: { IDENTIFIER_LIST: identifierList } + }); + } + + }); + } /** * Perform basic checks, common to all file types - namely that the parsed @@ -127,9 +201,9 @@ angular.module('import').factory('connectionParseService', /** * Returns a promise that will resolve to a transformer function that will - * take an object that may a "group" field, replacing it if present with a - * "parentIdentifier". If both a "group" and "parentIdentifier" field are - * present on the provided object, or if no group exists at the specified + * take an object that may contain a "group" field, replacing it if present + * with a "parentIdentifier". If both a "group" and "parentIdentifier" field + * are present on the provided object, or if no group exists at the specified * path, the function will throw a ParseError describing the failure. * * @returns {Promise.>} @@ -170,32 +244,109 @@ angular.module('import').factory('connectionParseService', }); } - // Translate a given javascript object to a full-fledged Connection - const connectionTransformer = connection => new Connection(connection); + /** + * Convert a provided ProtoConnection array into a ParseResult. Any provided + * transform functions will be run on each entry in `connectionData` before + * any other processing is done. + * + * @param {*[]} connectionData + * An arbitrary array of data. This must evaluate to a ProtoConnection + * object after being run through all functions in `transformFunctions`. + * + * @param {Function[]} transformFunctions + * An array of transformation functions to run on each entry in + * `connection` data. + * + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. + */ + function parseConnectionData(connectionData, transformFunctions) { + + // Check that the provided connection data array is not empty + const checkError = performBasicChecks(connectionData); + if (checkError) { + const deferred = $q.defer(); + deferred.reject(checkError); + return deferred.promise; + } - // Translate a Connection object to a patch requesting the creation of said - // Connection - const patchTransformer = connection => new DirectoryPatch({ - op: 'add', - path: '/', - value: connection - }); + return $q.all({ + groupTransformer : getGroupTransformer(), + invalidUserIdErrorDetector : getInvalidIdentifierErrorChecker( + userService.getUsers), + invalidGroupIDErrorDetector : getInvalidIdentifierErrorChecker( + userGroupService.getUserGroups), + }) + + // Transform the rows from the CSV file to an array of API patches + // and lists of user and group identifiers + .then(({groupTransformer, + invalidUserIdErrorDetector, invalidGroupIDErrorDetector}) => + connectionData.reduce((parseResult, data) => { + + const { patches, identifiers, users, groups } = parseResult; + + // Run the array data through each provided transform + let connectionObject = data; + _.forEach(transformFunctions, transform => { + connectionObject = transform(connectionObject); + }); + + // All errors found while parsing this connection + const connectionErrors = []; + parseResult.errors.push(connectionErrors); + + // Translate the group on the object to a parentIdentifier + try { + connectionObject = groupTransformer(connectionObject); + } + + // If there was a problem with the group or parentIdentifier + catch (error) { + connectionErrors.push(error); + } + + // Push any errors for invalid user or user group identifiers + const pushError = error => error && connectionErrors.push(error); + pushError(invalidUserIdErrorDetector(connectionObject.users)); + pushError(invalidGroupIDErrorDetector(connectionObject.userGroups)); + + // Add the user and group identifiers for this connection + users.push(connectionObject.users); + groups.push(connectionObject.groups); + + // Translate to a full-fledged Connection + const connection = new Connection(connectionObject); + + // Finally, add a patch for creating the connection + patches.push(new DirectoryPatch({ + op: 'add', + path: '/', + value: connection + })); + + // If there are any errors for this connection fail the whole batch + if (connectionErrors.length) + parseResult.hasErrors = true; + + return parseResult; + + }, new ParseResult())); + } /** * Convert a provided CSV representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. - * - * TODO: Describe disambiguation suffixes, e.g. hostname (parameter), and - * that we will accept without the suffix if it's unambigous. (or not? how about not?) + * object to be submitted to the PATCH REST endpoint, as well as a list of + * objects containing lists of user and user group identifiers to be granted + * to each connection. * * @param {String} csvData - * The JSON-encoded connection list to convert to a PATCH request body. + * The CSV-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided CSV. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseCSV = function parseCSV(csvData) { @@ -211,134 +362,69 @@ angular.module('import').factory('connectionParseService', catch(error) { console.error(error); const deferred = $q.defer(); - deferred.reject(error); + deferred.reject(new ParseError({ message: error.message })); return deferred.promise; } + // The header row - an array of string header values + const header = parsedData.length ? parsedData[0] : []; + // Slice off the header row to get the data rows const connectionData = parsedData.slice(1); - // Check that the provided CSV is not empty (the parser always - // returns an array) - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } - - // The header row - an array of string header values - const header = parsedData[0]; + // Generate the CSV transform function, and apply it to every row + // before applying all the rest of the standard transforms + return connectionCSVService.getCSVTransformer(header).then( + csvTransformer => - return $q.all({ - csvTransformer : connectionCSVService.getCSVTransformer(header), - groupTransformer : getGroupTransformer() - }) - - // Transform the rows from the CSV file to an array of API patches - .then(({csvTransformer, groupTransformer}) => connectionData.map( - dataRow => { - - // Translate the raw CSV data to a javascript object - let connectionObject = csvTransformer(dataRow); - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); + // Apply the CSV transform to every row + parseConnectionData(connectionData, [csvTransformer])); }; /** * Convert a provided YAML representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. + * object to be submitted to the PATCH REST endpoint, as well as a list of + * objects containing lists of user and user group identifiers to be granted + * to each connection. * * @param {String} yamlData - * The YAML-encoded connection list to convert to a PATCH request body. + * The YAML-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided YAML. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array - const parsedData = parseYAMLData(yamlData); + const connectionData = parseYAMLData(yamlData); - // Check that the data is the correct format, and not empty - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } - - // Transform the data from the YAML file to an array of API patches - return getGroupTransformer().then( - groupTransformer => parsedData.map(connectionObject => { - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); - + // Produce a ParseResult + return parseConnectionData(connectionData); }; /** - * Convert a provided JSON representation of a connection list into a JSON - * string to be submitted to the PATCH REST endpoint. The returned JSON - * string will contain a PATCH operation to create each connection in the - * provided list. + * Convert a provided JSON-encoded representation of a connection list into + * an array of patches to be submitted to the PATCH REST endpoint, as well + * as a list of objects containing lists of user and user group identifiers + * to be granted to each connection. * * @param {String} jsonData - * The JSON-encoded connection list to convert to a PATCH request body. + * The JSON-encoded connection list to process. * - * @return {Promise.} - * A promise resolving to an array of Connection objects, one for each - * connection in the provided JSON. + * @return {Promise.} + * A promise resolving to ParseResult object representing the result of + * parsing all provided connection data. */ service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array - const parsedData = JSON.parse(yamlData); + const connectionData = JSON.parse(jsonData); - // Check that the data is the correct format, and not empty - const checkError = performBasicChecks(connectionData); - if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; - } + // Produce a ParseResult + return parseConnectionData(connectionData); - // Transform the data from the YAML file to an array of API patches - return getGroupTransformer().then( - groupTransformer => parsedData.map(connectionObject => { - - // Translate the group on the object to a parentIdentifier - connectionObject = groupTransformer(connectionObject); - - // Translate to a full-fledged Connection - const connection = connectionTransformer(connectionObject); - - // Finally, translate to a patch for creating the connection - return patchTransformer(connection); - - })); - }; return service; diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseError.js b/guacamole/src/main/frontend/src/app/import/types/ParseError.js index 1e4ce478e..33fd07765 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseError.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseError.js @@ -31,7 +31,7 @@ angular.module('import').factory('ParseError', [function defineParseError() { * The object whose properties should be copied within the new * ParseError. */ - var ParseError = function ParseError(template) { + const ParseError = function ParseError(template) { // Use empty object by default template = template || {}; diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js new file mode 100644 index 000000000..e6aa59229 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -0,0 +1,87 @@ +/* + * 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. + */ + +/** + * Service which defines the ParseResult class. + */ +angular.module('import').factory('ParseResult', [function defineParseResult() { + + /** + * The result of parsing a connection import file - containing a list of + * API patches ready to be submitted to the PATCH REST API for batch + * connection creation, a set of users and user groups to grant access to + * each connection, and any errors that may have occured while parsing + * each connection. + * + * @constructor + * @param {ParseResult|Object} [template={}] + * The object whose properties should be copied within the new + * ParseResult. + */ + const ParseResult = function ParseResult(template) { + + // Use empty object by default + template = template || {}; + + /** + * An array of patches, ready to be submitted to the PATCH REST API for + * batch connection creation. + * + * @type {DirectoryPatch[]} + */ + this.patches = template.patches || []; + + /** + * A list of user identifiers that should be granted read access to the + * the corresponding connection (at the same array index). + * + * @type {String[]} + */ + this.users = template.users || []; + + /** + * A list of user group identifiers that should be granted read access + * to the corresponding connection (at the same array index). + * + * @type {String[]} + */ + this.groups = template.groups || []; + + /** + * An array of errors encountered while parsing the corresponding + * connection (at the same array index). Each connection should have a + * an array of errors. If empty, no errors occured for this connection. + * + * @type {ParseError[][]} + */ + this.errors = template.errors || []; + + /** + * True if any errors were encountered while parsing the connections + * represented by this ParseResult. This should always be true if there + * are a non-zero number of elements in the errors list for any + * connection, or false otherwise. + */ + this.hasErrors = template.hasErrors || false; + + }; + + return ParseResult; + +}]); diff --git a/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js b/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js new file mode 100644 index 000000000..c853934d0 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js @@ -0,0 +1,111 @@ +/* + * 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. + */ + +/** + * Service which defines the Connection class. + */ +angular.module('import').factory('ProtoConnection', [ + function defineProtoConnection() { + + /** + * A representation of a connection to be imported, as parsed from an + * user-supplied import file. + * + * @constructor + * @param {Connection|Object} [template={}] + * The object whose properties should be copied within the new + * Connection. + */ + var ProtoConnection = function ProtoConnection(template) { + + // Use empty object by default + template = template || {}; + + /** + * The unique identifier of the connection group that contains this + * connection. + * + * @type String + */ + this.parentIdentifier = template.parentIdentifier; + + /** + * The path to the connection group that contains this connection, + * written as e.g. "ROOT/parent/child/group". + * + * @type String + */ + this.group = template.group; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + * + * @type String + */ + this.name = template.name; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + * + * @type String + */ + this.protocol = template.protocol; + + /** + * Connection configuration parameters, as dictated by the protocol in + * use, arranged as name/value pairs. This information may not be + * available until directly queried. If this information is + * unavailable, this property will be null or undefined. + * + * @type Object. + */ + this.parameters = template.parameters || {}; + + /** + * Arbitrary name/value pairs which further describe this connection. + * The semantics and validity of these attributes are dictated by the + * extension which defines them. + * + * @type Object. + */ + this.attributes = template.attributes || {}; + + /** + * The identifiers of all users who should be granted read access to + * this connection. + * + * @type String[] + */ + this.users = template.users || []; + + /** + * The identifiers of all user groups who should be granted read access + * to this connection. + * + * @type String[] + */ + this.groups = template.groups || []; + + }; + + return ProtoConnection; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 98b983b9f..978ffc0e2 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -200,6 +200,10 @@ "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_FILE_TYPE": "Invalid import file type \"{TYPE}\"", + "ERROR_INVALID_USER_IDENTIFIERS": + "Users not found: {IDENTIFIER_LIST}", + "ERROR_INVALID_USER_GROUP_IDENTIFIERS": + "User Groups not found: {IDENTIFIER_LIST}", "ERROR_NO_FILE_SUPPLIED": "Please select a file to import", "ERROR_AMBIGUOUS_PARENT_GROUP": "Both group and parentIdentifier may be not specified at the same time", From bfb7f3b78a7699183d8255b6bf3415d9fc32c2b1 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Mon, 13 Feb 2023 19:02:26 +0000 Subject: [PATCH 153/289] GUACAMOLE-926: Create users and groups; don't require them to exist beforehand. --- .../importConnectionsController.js | 79 +++++++++- .../import/services/connectionCSVService.js | 8 +- .../import/services/connectionParseService.js | 135 +++++------------- ...ProtoConnection.js => ImportConnection.js} | 12 +- .../src/app/import/types/ParseResult.js | 17 +++ .../app/rest/services/connectionService.js | 6 +- .../src/app/rest/types/DirectoryPatch.js | 10 +- .../app/rest/types/DirectoryPatchOutcome.js | 10 +- .../app/rest/types/DirectoryPatchResponse.js | 50 +++++++ 9 files changed, 195 insertions(+), 132 deletions(-) rename guacamole/src/main/frontend/src/app/import/types/{ProtoConnection.js => ImportConnection.js} (91%) create mode 100644 guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 73164a08c..2d7cdab22 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -24,19 +24,73 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function importConnectionsController($scope, $injector) { // Required services + const $routeParams = $injector.get('$routeParams'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); // Required types + const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); const TranslatableMessage = $injector.get('TranslatableMessage'); + + /** + * Given a successful response to an import PATCH request, make another + * request to delete every created connection in the provided request, i.e. + * clean up every connection that was created. + * + * @param {DirectoryPatchResponse} creationResponse + */ + function cleanUpConnections(creationResponse) { + + // The patches to delete - one delete per initial creation + const deletionPatches = creationResponse.patches.map(patch => + new DirectoryPatch({ + op: 'remove', + path: '/' + patch.identifier + })); + + console.log("Deletion Patches", deletionPatches); + + connectionService.patchConnections( + $routeParams.dataSource, deletionPatches) - function handleSuccess(data) { - console.log("OMG SUCCESS: ", data) + .then(deletionResponse => + console.log("Deletion response", deletionResponse)) + .catch(handleParseError); + + } + + /** + * Process a successfully parsed import file, creating any specified + * connections, creating and granting permissions to any specified users + * and user groups. + * + * TODO: + * - Do batch import of connections + * - Create all users/groups not already present + * - Grant permissions to all users/groups as defined in the import file + * - On failure: Roll back everything (maybe ask the user first): + * - Attempt to delete all created connections + * - Attempt to delete any created users / groups + * + * @param {ParseResult} parseResult + * The result of parsing the user-supplied import file. + * + */ + function handleParseSuccess(parseResult) { + connectionService.patchConnections( + $routeParams.dataSource, parseResult.patches) + + .then(response => { + console.log("Creation Response", response); + + // TODON'T: Delete connections so we can test over and over + cleanUpConnections(response); + }); } // Set any caught error message to the scope for display - const handleError = error => { + const handleParseError = error => { console.error(error); $scope.error = error; } @@ -44,14 +98,25 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Clear the current error const clearError = () => delete $scope.error; - function processData(type, data) { + /** + * Process the uploaded import file, importing the connections, granting + * connection permissions, or displaying errors to the user if there are + * problems with the provided file. + * + * @param {String} mimeType + * The MIME type of the uploaded data file. + * + * @param {String} data + * The raw string contents of the import file. + */ + function processData(mimeType, data) { // The function that will process all the raw data and return a list of // patches to be submitted to the API let processDataCallback; // Parse the data based on the provided mimetype - switch(type) { + switch(mimeType) { case "application/json": case "text/json": @@ -82,10 +147,10 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ processDataCallback(data) // Send the data off to be imported if parsing is successful - .then(handleSuccess) + .then(handleParseSuccess) // Display any error found while parsing the file - .catch(handleError); + .catch(handleParseError); } $scope.upload = function() { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index a104cdd46..492b9388b 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -33,7 +33,7 @@ angular.module('import').factory('connectionCSVService', // Required types const ParseError = $injector.get('ParseError'); - const ProtoConnection = $injector.get('ProtoConnection'); + const ImportConnection = $injector.get('ImportConnection'); const TranslatableMessage = $injector.get('TranslatableMessage'); // Required services @@ -154,7 +154,7 @@ angular.module('import').factory('connectionCSVService', /** * Given a CSV header row, create and return a promise that will resolve to - * a function that can take a CSV data row and return a ProtoConnection + * a function that can take a CSV data row and return a ImportConnection * object. If an error occurs while parsing a particular row, the resolved * function will throw a ParseError describing the failure. * @@ -185,7 +185,7 @@ angular.module('import').factory('connectionCSVService', * * @returns {Promise.>} * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a ProtoConnection object. + * row (array of strings) to a ImportConnection object. */ service.getCSVTransformer = function getCSVTransformer(headerRow) { @@ -405,7 +405,7 @@ angular.module('import').factory('connectionCSVService', const parentIdentifier = ( parentIdentifierGetter && parentIdentifierGetter(row)); - return new ProtoConnection({ + return new ImportConnection({ // Fields that are not protocol-specific ...{ diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index f2f6c1daa..7c4f6c754 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -42,81 +42,8 @@ angular.module('import').factory('connectionParseService', const schemaService = $injector.get('schemaService'); const connectionCSVService = $injector.get('connectionCSVService'); const connectionGroupService = $injector.get('connectionGroupService'); - const userService = $injector.get('userService'); - const userGroupService = $injector.get('userGroupService'); const service = {}; - - /** - * Resolves to an object whose keys are all valid identifiers in the current - * data source. The provided `requestFunction` should resolve to such an - * object when provided with the current data source. - * - * @param {Function.>} requestFunction - * A function that, given a data source, will return a promise resolving - * to an object with keys that are unique identifiers for entities in - * that data source. - * - * @returns {Promise.>} - * A promise that will resolve to an function that, given an array of - * identifiers, will return all identifiers that do not exist as keys in - * the object returned by `requestFunction`, i.e. all identifiers that - * do not exist in the current data source. - */ - function getIdentifierMap(requestFunction) { - - // The current data source to which all the identifiers will belong - const dataSource = $routeParams.dataSource; - - // Make the request and return the response, which should be an object - // whose keys are all valid identifiers for the current data source - return requestFunction(dataSource); - } - - /** - * Return a promise that resolves to a function that takes an array of - * identifiers, returning a ParseError describing the invalid identifiers - * from the list. The provided `requestFunction` should resolve to such an - * object whose keys are all valid identifiers when provided with the - * current data source. - * - * @param {Function.>} requestFunction - * A function that, given a data source, will return a promise resolving - * to an object with keys that are unique identifiers for entities in - * that data source. - * - * @returns {Promise.>} - * A promise that will resolve to a function to check the validity of - * each identifier in a provided array, returning a ParseError - * describing the problem if any are not valid. - */ - function getInvalidIdentifierErrorChecker(requestFunction) { - - // Fetch all the valid user identifiers in the system, and - return getIdentifierMap(requestFunction).then(validIdentifiers => - - // The resolved function that takes a list of user group identifiers - allIdentifiers => { - - // Filter to only include invalid identifiers - const invalidIdentifiers = _.filter(allIdentifiers, - identifier => !validIdentifiers[identifier]); - - if (invalidIdentifiers.length) { - - // Quote and comma-seperate for display - const identifierList = invalidIdentifiers.map( - identifier => '"' + identifier + '"').join(', '); - - return new ParseError({ - message: 'Invalid User Group Identifiers: ' + identifierList, - key: 'CONNECTION_IMPORT.ERROR_INVALID_USER_GROUP_IDENTIFIERS', - variables: { IDENTIFIER_LIST: identifierList } - }); - } - - }); - } /** * Perform basic checks, common to all file types - namely that the parsed @@ -245,12 +172,12 @@ angular.module('import').factory('connectionParseService', } /** - * Convert a provided ProtoConnection array into a ParseResult. Any provided + * Convert a provided ImportConnection array into a ParseResult. Any provided * transform functions will be run on each entry in `connectionData` before * any other processing is done. * * @param {*[]} connectionData - * An arbitrary array of data. This must evaluate to a ProtoConnection + * An arbitrary array of data. This must evaluate to a ImportConnection * object after being run through all functions in `transformFunctions`. * * @param {Function[]} transformFunctions @@ -271,21 +198,10 @@ angular.module('import').factory('connectionParseService', return deferred.promise; } - return $q.all({ - groupTransformer : getGroupTransformer(), - invalidUserIdErrorDetector : getInvalidIdentifierErrorChecker( - userService.getUsers), - invalidGroupIDErrorDetector : getInvalidIdentifierErrorChecker( - userGroupService.getUserGroups), - }) + return getGroupTransformer().then(groupTransformer => + connectionData.reduce((parseResult, data) => { - // Transform the rows from the CSV file to an array of API patches - // and lists of user and group identifiers - .then(({groupTransformer, - invalidUserIdErrorDetector, invalidGroupIDErrorDetector}) => - connectionData.reduce((parseResult, data) => { - - const { patches, identifiers, users, groups } = parseResult; + const { patches, users, groups, allUsers, allGroups } = parseResult; // Run the array data through each provided transform let connectionObject = data; @@ -307,14 +223,15 @@ angular.module('import').factory('connectionParseService', connectionErrors.push(error); } - // Push any errors for invalid user or user group identifiers - const pushError = error => error && connectionErrors.push(error); - pushError(invalidUserIdErrorDetector(connectionObject.users)); - pushError(invalidGroupIDErrorDetector(connectionObject.userGroups)); - // Add the user and group identifiers for this connection - users.push(connectionObject.users); - groups.push(connectionObject.groups); + const connectionUsers = connectionObject.users || []; + const connectionGroups = connectionObject.groups || []; + users.push(connectionUsers); + groups.push(connectionGroups); + + // Add all user and user group identifiers to the overall sets + connectionUsers.forEach(identifier => allUsers[identifier] = true); + connectionGroups.forEach(identifier => allGroups[identifier] = true); // Translate to a full-fledged Connection const connection = new Connection(connectionObject); @@ -398,7 +315,18 @@ angular.module('import').factory('connectionParseService', service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array - const connectionData = parseYAMLData(yamlData); + try { + const connectionData = parseYAMLData(yamlData); + } + + // If the YAML parser throws an error, reject with that error. No + // translation key will be available here. + catch(error) { + console.error(error); + const deferred = $q.defer(); + deferred.reject(new ParseError({ message: error.message })); + return deferred.promise; + } // Produce a ParseResult return parseConnectionData(connectionData); @@ -420,7 +348,18 @@ angular.module('import').factory('connectionParseService', service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array - const connectionData = JSON.parse(jsonData); + try { + const connectionData = JSON.parse(jsonData); + } + + // If the JSON parse attempt throws an error, reject with that error. + // No translation key will be available here. + catch(error) { + console.error(error); + const deferred = $q.defer(); + deferred.reject(new ParseError({ message: error.message })); + return deferred.promise; + } // Produce a ParseResult return parseConnectionData(connectionData); diff --git a/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js similarity index 91% rename from guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js rename to guacamole/src/main/frontend/src/app/import/types/ImportConnection.js index c853934d0..9f336ef77 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ProtoConnection.js +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js @@ -18,21 +18,21 @@ */ /** - * Service which defines the Connection class. + * Service which defines the ImportConnection class. */ -angular.module('import').factory('ProtoConnection', [ - function defineProtoConnection() { +angular.module('import').factory('ImportConnection', [ + function defineImportConnection() { /** * A representation of a connection to be imported, as parsed from an * user-supplied import file. * * @constructor - * @param {Connection|Object} [template={}] + * @param {ImportConnection|Object} [template={}] * The object whose properties should be copied within the new * Connection. */ - var ProtoConnection = function ProtoConnection(template) { + var ImportConnection = function ImportConnection(template) { // Use empty object by default template = template || {}; @@ -106,6 +106,6 @@ angular.module('import').factory('ProtoConnection', [ }; - return ProtoConnection; + return ImportConnection; }]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js index e6aa59229..77c49eb31 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -63,6 +63,23 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { */ this.groups = template.groups || []; + /** + * An object whose keys are the user identifiers of every user specified + * in the batch import. i.e. a set of all user identifiers. + * + * @type {Object.} + */ + this.allUsers = template.allUsers || {}; + + /** + * An object whose keys are the user group identifiers of every user + * group specified in the batch import. i.e. a set of all user group + * identifiers. + * + * @type {Object.} + */ + this.allGroups = template.allGroups || {}; + /** * An array of errors encountered while parsing the corresponding * connection (at the same array index). Each connection should have a diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index 053559530..5c1451ec2 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -158,7 +158,7 @@ angular.module('rest').factory('connectionService', ['$injector', /** * Makes a request to the REST API to apply a supplied list of connection * patches, returning a promise that can be used for processing the results - * of the call. + * of the call. * * This operation is atomic - if any errors are encountered during the * connection patching process, the entire request will fail, and no @@ -181,12 +181,14 @@ angular.module('rest').factory('connectionService', ['$injector', }) // Clear the cache - .then(function connectionsPatched(){ + .then(function connectionsPatched(patchResponse){ cacheService.connections.removeAll(); // Clear users cache to force reload of permissions for any // newly created or replaced connections cacheService.users.removeAll(); + + return patchResponse; }); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js index 96a2cb093..05874d4fc 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js @@ -59,10 +59,9 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch( this.path = template.path || '/'; /** - * The object being added or replaced, or the identifier of the object - * being removed. + * The object being added, or undefined if deleting. * - * @type {DirectoryObject|String} + * @type {DirectoryObject} */ this.value = template.value; @@ -78,11 +77,6 @@ angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch( */ ADD : "add", - /** - * Removes the specified object from the relation. - */ - REPLACE : "replace", - /** * Removes the specified object from the relation. */ diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js index 4534dae13..e6710c2ca 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js @@ -30,15 +30,11 @@ angular.module('rest').factory('DirectoryPatchOutcome', [ * response. The error field is only meaningful for unsuccessful patches. * @constructor * - * @template DirectoryObject - * The directory-based object type that this DirectoryPatchOutcome - * represents a patch outcome for. - * - * @param {DirectoryObject|Object} [template={}] + * @param {DirectoryPatchOutcome|Object} [template={}] * The object whose properties should be copied within the new * DirectoryPatchOutcome. */ - var DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { + const DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { // Use empty object by default template = template || {}; @@ -78,6 +74,6 @@ angular.module('rest').factory('DirectoryPatchOutcome', [ }; - return DirectoryPatch; + return DirectoryPatchOutcome; }]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js new file mode 100644 index 000000000..9538c077e --- /dev/null +++ b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Service which defines the DirectoryPatchResponse class. + */ +angular.module('rest').factory('DirectoryPatchResponse', [ + function defineDirectoryPatchResponse() { + + /** + * An object returned by a PATCH request to a directory REST API, + * representing the successful response to a patch request. + * + * @param {DirectoryPatchResponse|Object} [template={}] + * The object whose properties should be copied within the new + * DirectoryPatchResponse. + */ + const DirectoryPatchResponse = function DirectoryPatchResponse(template) { + + // Use empty object by default + template = template || {}; + + /** + * An outcome for each patch in the corresponding patch request. + * + * @type {DirectoryPatchOutcome[]} + */ + this.patches = template.patches; + + }; + + return DirectoryPatchResponse; + +}]); From 51e0fb8c661373afadb7e09e8965076db8057bf8 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 14 Feb 2023 02:12:20 +0000 Subject: [PATCH 154/289] GUACAMOLE-926: Grant permissions to all users / groups. --- .../importConnectionsController.js | 165 ++++++++++++++++-- .../import/services/connectionParseService.js | 34 +++- .../src/app/import/types/ParseResult.js | 27 +-- .../src/app/rest/services/userGroupService.js | 33 ++++ .../src/app/rest/services/userService.js | 33 ++++ 5 files changed, 253 insertions(+), 39 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 2d7cdab22..8cd235e6e 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -17,6 +17,8 @@ * under the License. */ +/* global _ */ + /** * The controller for the connection import page. */ @@ -24,14 +26,21 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function importConnectionsController($scope, $injector) { // Required services + const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); + const permissionService = $injector.get('permissionService'); + const userService = $injector.get('userService'); + const userGroupService = $injector.get('userGroupService'); // Required types const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); + const PermissionSet = $injector.get('PermissionSet'); const TranslatableMessage = $injector.get('TranslatableMessage'); + const User = $injector.get('User'); + const UserGroup = $injector.get('UserGroup'); /** * Given a successful response to an import PATCH request, make another @@ -56,10 +65,133 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(deletionResponse => console.log("Deletion response", deletionResponse)) - .catch(handleParseError); + .catch(handleError); } + /** + * Create all users and user groups mentioned in the import file that don't + * already exist in the current data source. + * + * @param {ParseResult} parseResult + * The result of parsing the user-supplied import file. + * + * @return {Object} + * An object containing the results of the calls to create the users + * and groups. + */ + function createUsersAndGroups(parseResult) { + + const dataSource = $routeParams.dataSource; + + return $q.all({ + existingUsers : userService.getUsers(dataSource), + existingGroups : userGroupService.getUserGroups(dataSource) + }).then(({existingUsers, existingGroups}) => { + + const userPatches = Object.keys(parseResult.users) + + // Filter out any existing users + .filter(identifier => !existingUsers[identifier]) + + // A patch to create each new user + .map(username => new DirectoryPatch({ + op: 'add', + path: '/', + value: new User({ username }) + })); + + const groupPatches = Object.keys(parseResult.groups) + + // Filter out any existing groups + .filter(identifier => !existingGroups[identifier]) + + // A patch to create each new user group + .map(identifier => new DirectoryPatch({ + op: 'add', + path: '/', + value: new UserGroup({ identifier }) + })); + + return $q.all({ + createdUsers: userService.patchUsers(dataSource, userPatches), + createdGroups: userGroupService.patchUserGroups(dataSource, groupPatches) + }); + + }); + + } + + /** + * Grant read permissions for each user and group in the supplied parse + * result to each connection in their connection list. Note that there will + * be a seperate request for each user and group. + * + * @param {ParseResult} parseResult + * The result of successfully parsing a user-supplied import file. + * + * @param {Object} response + * The response from the PATCH API request. + * + * @returns {Promise.} + * A promise that will resolve with the result of every permission + * granting request. + */ + function grantConnectionPermissions(parseResult, response) { + + const dataSource = $routeParams.dataSource; + + // All connection grant requests, one per user/group + const userRequests = {}; + const groupRequests = {}; + + // Create a PermissionSet granting access to all connections at + // the provided indices within the provided parse result + const createPermissionSet = indices => + new PermissionSet({ connectionPermissions: indices.reduce( + (permissions, index) => { + const connectionId = response.patches[index].identifier; + permissions[connectionId] = [ + PermissionSet.ObjectPermissionType.READ]; + return permissions; + }, {}) }); + + // Now that we've created all the users, grant access to each + _.forEach(parseResult.users, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `false` + userRequests[identifier] = permissionService.patchPermissions( + dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is not for a group + false)); + + // Now that we've created all the groups, grant access to each + _.forEach(parseResult.groups, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `true` + groupRequests[identifier] = permissionService.patchPermissions( + dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is for a group + true)); + + // Return the result from all the permission granting calls + return $q.all({ ...userRequests, ...groupRequests }); + } + /** * Process a successfully parsed import file, creating any specified * connections, creating and granting permissions to any specified users @@ -78,19 +210,32 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * */ function handleParseSuccess(parseResult) { - connectionService.patchConnections( - $routeParams.dataSource, parseResult.patches) - - .then(response => { - console.log("Creation Response", response); - // TODON'T: Delete connections so we can test over and over - cleanUpConnections(response); + const dataSource = $routeParams.dataSource; + + console.log("parseResult", parseResult); + + // First, attempt to create the connections + connectionService.patchConnections(dataSource, parseResult.patches) + .then(response => { + + // If connection creation is successful, create users and groups + createUsersAndGroups(parseResult).then(() => { + + grantConnectionPermissions(parseResult, response).then(results => { + console.log("permission requests", results); + + // TODON'T: Delete connections so we can test over and over + cleanUpConnections(response); + }) + + + }); }); } // Set any caught error message to the scope for display - const handleParseError = error => { + const handleError = error => { console.error(error); $scope.error = error; } @@ -150,7 +295,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(handleParseSuccess) // Display any error found while parsing the file - .catch(handleParseError); + .catch(handleError); } $scope.upload = function() { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 7c4f6c754..4b9f50c2d 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -199,9 +199,9 @@ angular.module('import').factory('connectionParseService', } return getGroupTransformer().then(groupTransformer => - connectionData.reduce((parseResult, data) => { + connectionData.reduce((parseResult, data, index) => { - const { patches, users, groups, allUsers, allGroups } = parseResult; + const { patches, users, groups } = parseResult; // Run the array data through each provided transform let connectionObject = data; @@ -223,15 +223,33 @@ angular.module('import').factory('connectionParseService', connectionErrors.push(error); } - // Add the user and group identifiers for this connection + // The users and user groups that should be granted access const connectionUsers = connectionObject.users || []; const connectionGroups = connectionObject.groups || []; - users.push(connectionUsers); - groups.push(connectionGroups); - // Add all user and user group identifiers to the overall sets - connectionUsers.forEach(identifier => allUsers[identifier] = true); - connectionGroups.forEach(identifier => allGroups[identifier] = true); + // Add this connection index to the list for each user + connectionUsers.forEach(identifier => { + + // If there's an existing list, add the index to that + if (users[identifier]) + users[identifier].push(index); + + // Otherwise, create a new list with just this index + else + users[identifier] = [index]; + }); + + // Add this connection index to the list for each group + connectionGroups.forEach(identifier => { + + // If there's an existing list, add the index to that + if (groups[identifier]) + groups[identifier].push(index); + + // Otherwise, create a new list with just this index + else + groups[identifier] = [index]; + }); // Translate to a full-fledged Connection const connection = new Connection(connectionObject); diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js index 77c49eb31..1a5abd14d 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js +++ b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js @@ -48,28 +48,13 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { this.patches = template.patches || []; /** - * A list of user identifiers that should be granted read access to the - * the corresponding connection (at the same array index). + * An object whose keys are the user identifiers of users specified + * in the batch import. and the keys are an array of indices of + * connections to which those users should be granted access. * - * @type {String[]} + * @type {Object.} */ - this.users = template.users || []; - - /** - * A list of user group identifiers that should be granted read access - * to the corresponding connection (at the same array index). - * - * @type {String[]} - */ - this.groups = template.groups || []; - - /** - * An object whose keys are the user identifiers of every user specified - * in the batch import. i.e. a set of all user identifiers. - * - * @type {Object.} - */ - this.allUsers = template.allUsers || {}; + this.users = template.users || {}; /** * An object whose keys are the user group identifiers of every user @@ -78,7 +63,7 @@ angular.module('import').factory('ParseResult', [function defineParseResult() { * * @type {Object.} */ - this.allGroups = template.allGroups || {}; + this.groups = template.users || {}; /** * An array of errors encountered while parsing the corresponding diff --git a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js b/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js index 090efa944..987f8383a 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js @@ -190,6 +190,39 @@ angular.module('rest').factory('userGroupService', ['$injector', }; + /** + * Makes a request to the REST API to apply a supplied list of user group + * patches, returning a promise that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchUserGroups = function patchUserGroups(dataSource, patches) { + + // Make the PATCH request + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + data : patches + }) + + // Clear the cache + .then(function userGroupsPatched(patchResponse){ + cacheService.users.removeAll(); + return patchResponse; + }); + + } + return service; }]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/userService.js b/guacamole/src/main/frontend/src/app/rest/services/userService.js index faa26b40a..252aead3a 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/userService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/userService.js @@ -235,6 +235,39 @@ angular.module('rest').factory('userService', ['$injector', }); }; + + /** + * Makes a request to the REST API to apply a supplied list of user patches, + * returning a promise that can be used for processing the results of the + * call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param {DirectoryPatch.[]} patches + * An array of patches to apply. + * + * @returns {Promise} + * A promise for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + service.patchUsers = function patchUsers(dataSource, patches) { + + // Make the PATCH request + return authenticationService.request({ + method : 'PATCH', + url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users', + data : patches + }) + + // Clear the cache + .then(function usersPatched(patchResponse){ + cacheService.users.removeAll(); + return patchResponse; + }); + + } return service; From a1c1a5888612a0b05b7741dc27c7ec8d8b30dd2d Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 16 Feb 2023 01:30:55 +0000 Subject: [PATCH 155/289] GUACAMOLE-926: Create new file upload UI. --- .../importConnectionsController.js | 240 ++++++++++++++--- .../directives/connectionImportFileUpload.js | 253 ++++++++++++++++++ .../src/app/import/styles/file-upload.css | 119 ++++++++ .../frontend/src/app/import/styles/import.css | 9 + .../import/templates/connectionImport.html | 16 +- .../templates/connectionImportFileUpload.html | 30 +++ .../main/frontend/src/translations/en.json | 14 +- guacamole/src/main/frontend/webpack.config.js | 16 +- 8 files changed, 641 insertions(+), 56 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js create mode 100644 guacamole/src/main/frontend/src/app/import/styles/file-upload.css create mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 8cd235e6e..31a49adf8 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -42,6 +42,70 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const User = $injector.get('User'); const UserGroup = $injector.get('UserGroup'); + /** + * Any error that may have occured during import file parsing. + * + * @type {ParseError} + */ + $scope.error = null; + + /** + * True if the file is fully uploaded and ready to be processed, or false + * otherwise. + * + * @type {Boolean} + */ + $scope.dataReady = false; + + /** + * True if the file upload has been aborted mid-upload, or false otherwise. + */ + $scope.aborted = false; + + /** + * True if fully-uploaded data is being processed, or false otherwise. + */ + $scope.processing = false; + + /** + * The MIME type of the uploaded file, if any. + * + * @type {String} + */ + $scope.mimeType = null; + + /** + * The raw string contents of the uploaded file, if any. + * + * @type {String} + */ + $scope.fileData = null; + + /** + * The file reader currently being used to upload the file, if any. If + * null, no file upload is currently in progress. + * + * @type {FileReader} + */ + $scope.fileReader = null; + + /** + * Clear all file upload state. + */ + function resetUploadState() { + + $scope.aborted = false; + $scope.dataReady = false; + $scope.processing = false; + $scope.fileData = null; + $scope.mimeType = null; + $scope.fileReader = null; + + // Broadcast an event to clear the file upload UI + $scope.$broadcast('clearFile'); + + } + /** * Given a successful response to an import PATCH request, make another * request to delete every created connection in the provided request, i.e. @@ -227,20 +291,35 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // TODON'T: Delete connections so we can test over and over cleanUpConnections(response); - }) + resetUploadState(); + }) }); }); } - // Set any caught error message to the scope for display + /** + * Set any caught error message to the scope for display. + * + * @argument {ParseError} error + * The error to display. + */ const handleError = error => { + + // Any error indicates that processing of the file has failed, so clear + // all upload state to allow for a fresh retry + resetUploadState(); + + // Set the error for display console.error(error); $scope.error = error; + } - // Clear the current error + /** + * Clear the current displayed error. + */ const clearError = () => delete $scope.error; /** @@ -255,39 +334,35 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * The raw string contents of the import file. */ function processData(mimeType, data) { + + // Data processing has begun + $scope.processing = true; // The function that will process all the raw data and return a list of // patches to be submitted to the API let processDataCallback; - // Parse the data based on the provided mimetype - switch(mimeType) { + // Choose the appropriate parse function based on the mimetype + if (mimeType.endsWith("json")) + processDataCallback = connectionParseService.parseJSON; - case "application/json": - case "text/json": - processDataCallback = connectionParseService.parseJSON; - break; + else if (mimeType.endsWith("csv")) + processDataCallback = connectionParseService.parseCSV; - case "text/csv": - processDataCallback = connectionParseService.parseCSV; - break; + else if (mimeType.endsWith("yaml")) + processDataCallback = connectionParseService.parseYAML; - case "application/yaml": - case "application/x-yaml": - case "text/yaml": - case "text/x-yaml": - processDataCallback = connectionParseService.parseYAML; - break; - - default: - handleError(new ParseError({ - message: 'Invalid file type: ' + type, - key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', - variables: { TYPE: type } - })); - return; + // We don't expect this to happen - the file upload directive should + // have already have filtered out any invalid file types + else { + handleError(new ParseError({ + message: 'Invalid file type: ' + type, + key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', + variables: { TYPE: type } + })); + return; } - + // Make the call to process the data into a series of patches processDataCallback(data) @@ -298,30 +373,109 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .catch(handleError); } - $scope.upload = function() { + /** + * Process the uploaded import data. Only usuable if the upload is fully + * complete. + */ + $scope.import = () => processData($scope.mimeType, $scope.fileData); + + /** + * @return {Boolean} + * True if import should be disabled, or false if cancellation + * should be allowed. + */ + $scope.importDisabled = () => + + // Disable import if no data is ready + !$scope.dataReady || + + // Disable import if the file is currently being processed + $scope.processing; + + /** + * Cancel any in-progress upload, or clear any uploaded-but + */ + $scope.cancel = function() { + + // Clear any error message + clearError(); + + // If the upload is in progress, stop it now; the FileReader will + // reset the upload state when it stops + if ($scope.fileReader) { + $scope.aborted = true; + $scope.fileReader.abort(); + } + + // Clear any upload state - there's no FileReader handler to do it + else + resetUploadState(); + + }; + + /** + * @return {Boolean} + * True if cancellation should be disabled, or false if cancellation + * should be allowed. + */ + $scope.cancelDisabled = () => + + // Disable cancellation if the import has already been cancelled + $scope.aborted || + + // Disable cancellation if the file is currently being processed + $scope.processing || + + // Disable cancellation if no data is ready or being uploaded + !($scope.fileReader || $scope.dataReady); + + /** + * Handle a provided File upload, reading all data onto the scope for + * import processing, should the user request an import. + * + * @argument {File} file + * The file to upload onto the scope for further processing. + */ + $scope.handleFile = function(file) { // Clear any error message from the previous upload attempt clearError(); - const files = angular.element('#file')[0].files; + // Initialize upload state + $scope.aborted = false; + $scope.dataReady = false; + $scope.processing = false; + $scope.uploadStarted = true; - if (files.length <= 0) { - handleError(new ParseError({ - message: 'No file supplied', - key: 'CONNECTION_IMPORT.ERROR_NO_FILE_SUPPLIED' - })); - return; - } + // Save the MIME type to the scope + $scope.mimeType = file.type; - // The file that the user uploaded - const file = files[0]; + // Save the file to the scope when ready + $scope.fileReader = new FileReader(); + $scope.fileReader.onloadend = (e => { - // Call processData when the data is ready - const reader = new FileReader(); - reader.onloadend = (e => processData(file.type, e.target.result)); + // If the upload was explicitly aborted, clear any upload state and + // do not process the data + if ($scope.aborted) + resetUploadState(); - // Read all the data into memory and call processData when done - reader.readAsBinaryString(file); + else { + + // Save the uploaded data + $scope.fileData = e.target.result; + + // Mark the data as ready + $scope.dataReady = true; + + // Clear the file reader from the scope now that this file is + // fully uploaded + $scope.fileReader = null; + + } + }); + + // Read all the data into memory + $scope.fileReader.readAsBinaryString(file); } }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js new file mode 100644 index 000000000..d988e7c7d --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -0,0 +1,253 @@ +/* + * 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. + */ + +/* global _ */ + +/** + * A directive that allows for file upload, either through drag-and-drop or + * a file browser. + */ + +/** + * All legal import file types. Any file not belonging to one of these types + * must be rejected. + */ +const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; + +angular.module('import').directive('connectionImportFileUpload', [ + function connectionImportFileUpload() { + + const directive = { + restrict: 'E', + replace: true, + templateUrl: 'app/import/templates/connectionImportFileUpload.html', + scope: { + + /** + * The function to invoke when a file is provided to the file upload + * UI, either by dragging and dropping, or by navigating using the + * file browser. The function will be called with 2 arguments - the + * mime type, and the raw string contents of the file. + * + * @type function + */ + onFile : '&', + } + }; + + directive.controller = ['$scope', '$injector', '$element', + function fileUploadController($scope, $injector, $element) { + + // Required services + const $timeout = $injector.get('$timeout'); + + /** + * Whether a drag/drop operation is currently in progress (the user has + * dragged a file over the Guacamole connection but has not yet + * dropped it). + * + * @type boolean + */ + $scope.dropPending = false; + + /** + * The error associated with the file upload, if any. An object of the + * form { key, variables }, or null if no error has occured. + */ + $scope.error = null; + + /** + * The name of the file that's currently being uploaded, or has yet to + * be imported, if any. + */ + $scope.fileName = null; + + // Clear the file if instructed to do so by the parent + $scope.$on('clearFile', () => delete $scope.fileName); + + /** + * Clear any displayed error message. + */ + const clearError = () => $scope.error = null; + + /** + * Set an error for display using the provided translation key and + * translation variables. + * + * @param {String} key + * The translation key. + * + * @param {Object.} variables + * The variables to subsitute into the message, if any. + */ + const setError = (key, variables) => $scope.error = { key, variables }; + + /** + * The location where files can be dragged-and-dropped to. + * + * @type Element + */ + const dropTarget = $element.find('.drop-target')[0]; + + /** + * Displays a visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the in-progress drag/drop operation. + */ + const notifyDragStart = function notifyDragStart(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = true; + }); + + }; + + /** + * Removes the visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the end of the former drag/drop operation. + */ + const notifyDragEnd = function notifyDragEnd(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = false; + }); + + }; + + // Add listeners to the drop target to ensure that the visual state + // stays up to date + dropTarget.addEventListener('dragenter', notifyDragStart, false); + dropTarget.addEventListener('dragover', notifyDragStart, false); + dropTarget.addEventListener('dragleave', notifyDragEnd, false); + + /** + * Given a user-supplied file, validate that the file type is correct, + * and invoke the onFile callback provided to this directive if so. + * + * @param {File} file + * The user-supplied file. + */ + function handleFile(file) { + + // Clear any error from a previous attempted file upload + clearError(); + + // The MIME type of the provided file + const mimeType = file.type; + + // Check if the mimetype ends with one of the supported types, + // e.g. "application/json" or "text/csv" + if (_.every(LEGAL_FILE_TYPES.map( + type => !mimeType.endsWith(type)))) { + + // If the provided file is not one of the supported types, + // display an error and abort processing + setError('CONNECTION_IMPORT.ERROR_INVALID_FILE_TYPE', + { TYPE: mimeType }); + return; + } + + $scope.fileName = file.name; + + // Invoke the provided file callback using the file + $scope.onFile({ file }); + } + + /** + * Drop target event listener that will be invoked if the user drops + * anything onto the drop target. If a valid file is provided, the + * onFile callback provided to this directive will be called; otherwise + * an error will be displayed, if appropriate. + * + * @param {Event} e + * The drop event that triggered this handler. + */ + dropTarget.addEventListener('drop', function(e) { + + notifyDragEnd(e); + + const files = e.dataTransfer.files; + + // Ignore any non-files that are dragged into the drop area + if (files.length < 1) + return; + + if (files.length > 2) { + + // If more than one file was provided, print an error explaining + // that only a single file is allowed and abort processing + setError('CONNECTION_IMPORT.ERROR_FILE_SINGLE_ONLY'); + return; + } + + handleFile(files[0]); + + }, false); + + /** + * The hidden file input used to create a file browser. + * + * @type Element + */ + const fileUploadInput = $element.find('.file-upload-input')[0]; + + /** + * A function that will click on the hidden file input to open a file + * browser to allow the user to select a file for upload. + */ + $scope.openFileBrowser = () => + $timeout(() => fileUploadInput.click(), 0, false); + + /** + * A handler that will be invoked when a user selectes a file in the + * file browser. After some error checking, the file will be passed to + * the onFile callback provided to this directive. + * + * @param {Event} e + * The event that was triggered when the user selected a file in + * their file browser. + */ + fileUploadInput.onchange = e => { + + // Process the uploaded file + handleFile(e.target.files[0]); + + // Clear the value to ensure that the change event will be fired + // if the user selects the same file again + fileUploadInput.value = null; + + }; + + }]; + return directive; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css b/guacamole/src/main/frontend/src/app/import/styles/file-upload.css new file mode 100644 index 000000000..afe0fcf97 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/styles/file-upload.css @@ -0,0 +1,119 @@ +/* + * 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. + */ + +.file-upload-container { + + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 24px 24px; + + width: fit-content; + + border: 1px solid rgba(0,0,0,.25); + box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); + + margin-left: auto; + margin-right: auto; + +} + +.file-upload-container .upload-header { + + display: flex; + flex-direction: row; + width: 500px; + margin-bottom: 5px; + justify-content: space-between; + +} + +.file-upload-container .file-error { + + color: red; + +} + +.file-upload-container .file-options { + + font-weight: bold; + +} + +.file-upload-container .file-upload-input { + + display: none; + +} + +.file-upload-container .drop-target { + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: space-evenly; + + width: 500px; + height: 200px; + + background: rgba(0,0,0,.04); + border: 1px solid black; + +} + +.file-upload-container .drop-target.file-present { + + background: rgba(0,0,0,.15); + +} + + +.file-upload-container .drop-target .file-name { + + font-weight: bold; + font-size: 1.5em; + +} + +.file-upload-container .drop-target.drop-pending { + + background: #3161a9; + +} + +.file-upload-container .drop-target.drop-pending > * { + + opacity: 0.5; + +} + +.file-upload-container .drop-target .title { + + font-weight: bold; + font-size: 1.25em; + +} + +.file-upload-container .drop-target .browse-link { + + text-decoration: underline; + cursor: pointer; + +} diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index f5551bb42..5addb1933 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -19,4 +19,13 @@ .import .parseError { color: red; +} + +.import .import-buttons { + + margin-top: 10px; + display: flex; + gap: 10px; + justify-content: center; + } \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index 39b12fece..881cb2429 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -5,9 +5,19 @@ - - - + + +
      + + +
      +

      + +

      + {{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}} + +
      + +
      + +
      {{'CONNECTION_IMPORT.UPLOAD_DROP_TITLE' | translate}}
      + + + + {{'CONNECTION_IMPORT.UPLOAD_BROWSE_LINK' | translate}} + + +
      {{fileName}}
      + +
      + + +

      + + diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 978ffc0e2..73afddb3c 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -186,6 +186,9 @@ "CONNECTION_IMPORT": { + "BUTTON_CANCEL": "Cancel", + "BUTTON_IMPORT": "Import Connections", + "HEADER": "Connection Import", "ERROR_AMBIGUOUS_CSV_HEADER": @@ -199,7 +202,7 @@ "Invalid CSV Header \"{HEADER}\" is neither an attribute or parameter", "ERROR_INVALID_GROUP": "No group matching \"{GROUP}\" found", "ERROR_INVALID_FILE_TYPE": - "Invalid import file type \"{TYPE}\"", + "Unsupported file type: \"{TYPE}\"", "ERROR_INVALID_USER_IDENTIFIERS": "Users not found: {IDENTIFIER_LIST}", "ERROR_INVALID_USER_GROUP_IDENTIFIERS": @@ -210,7 +213,14 @@ "ERROR_REQUIRED_PROTOCOL": "No connection protocol found in the provided file", "ERROR_REQUIRED_NAME": - "No connection name found in the provided file" + "No connection name found in the provided file", + + "UPLOAD_FILE_TYPES": "CSV, JSON, or YAML", + "UPLOAD_HELP_LINK": "View Format Tips", + "UPLOAD_DROP_TITLE": "Drop a File Here", + "UPLOAD_BROWSE_LINK": "Browse for File", + + "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time" }, diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index dc6ad08cf..7f8f9da81 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -95,14 +95,14 @@ module.exports = { optimization: { minimizer: [ - // Minify using Google Closure Compiler - new ClosureWebpackPlugin({ mode: 'STANDARD' }, { - languageIn: 'ECMASCRIPT_2020', - languageOut: 'ECMASCRIPT5', - compilationLevel: 'SIMPLE' - }), - - new CssMinimizerPlugin() +// // Minify using Google Closure Compiler +// new ClosureWebpackPlugin({ mode: 'STANDARD' }, { +// languageIn: 'ECMASCRIPT_2020', +// languageOut: 'ECMASCRIPT5', +// compilationLevel: 'SIMPLE' +// }), +// +// new CssMinimizerPlugin() ], splitChunks: { From 690a8a34cabfb899d35e52201d6573657ab832c7 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 21 Feb 2023 22:14:02 +0000 Subject: [PATCH 156/289] GUACAMOLE-926: Add file format help page. --- .../frontend/src/app/import/styles/help.css | 60 ++++++++++++ .../templates/connectionImportFileHelp.html | 93 +++++++++++++++++++ .../templates/connectionImportFileUpload.html | 2 +- .../src/app/index/config/indexRouteConfig.js | 8 ++ .../main/frontend/src/translations/en.json | 18 ++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/frontend/src/app/import/styles/help.css create mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html diff --git a/guacamole/src/main/frontend/src/app/import/styles/help.css b/guacamole/src/main/frontend/src/app/import/styles/help.css new file mode 100644 index 000000000..e78acea4a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/styles/help.css @@ -0,0 +1,60 @@ +/* + * 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. + */ + +.import.help { + + text-transform: none; + +} + +.import.help p { + + max-width: 70em; + +} + +.import.help h2 { + + padding-top: 0px; + padding-bottom: 0px; + +} + +.import.help p, .import.help pre { + + margin-left: 1em; + +} + +.import.help pre { + + background-color: rgba(0,0,0,0.15); + padding: 10px; + width: fit-content; + +} + +.import.help .footnotes { + + border-top: 1px solid gray; + padding-top: 1em; + width: fit-content; + margin-left: 1em; + +} \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html new file mode 100644 index 000000000..bd7f21845 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -0,0 +1,93 @@ +
      + +
      +

      {{'CONNECTION_IMPORT.HELP_HEADER' | translate}}

      + +
      + +

      {{'CONNECTION_IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

      +

      {{'CONNECTION_IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

      + +

      {{'CONNECTION_IMPORT.HELP_CSV_HEADER' | translate}}

      +

      {{'CONNECTION_IMPORT.HELP_CSV_DESCRIPTION' | translate}}

      +

      {{'CONNECTION_IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

      +
      name,protocol,hostname,group,users,groups,guacd-encryption (attribute)
      +conn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none
      +conn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl
      +conn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,
      +conn4,kubernetes,,,,,
      + +

      {{'CONNECTION_IMPORT.HELP_JSON_HEADER' | translate}}

      +

      {{'CONNECTION_IMPORT.HELP_JSON_DESCRIPTION' | translate}}

      +

      {{'CONNECTION_IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

      +
      [
      +  {
      +    "name": "conn1",
      +    "protocol": "vnc",
      +    "parameters": { "hostname": "conn1.web.com" },
      +    "parentIdentifier": "ROOT",
      +    "users": [ "guac user 1", "guac user 2" ],
      +    "groups": [ "Connection 1 Users" ],
      +    "attributes": { "guacd-encryption": "none" }
      +  },
      +  {
      +    "name": "conn2",
      +    "protocol": "rdp",
      +    "parameters": { "hostname": "conn2.web.com" },
      +    "group": "ROOT/Parent Group",
      +    "users": [ "guac user 1" ],
      +    "attributes": { "guacd-encryption": "none" }
      +  },
      +  {
      +    "name": "conn3",
      +    "protocol": "ssh",
      +    "parameters": { "hostname": "conn3.web.com" },
      +    "group": "ROOT/Parent Group/Child Group",
      +    "users": [ "guac user 2", "guac user 3" ],
      +  },
      +  {
      +    "name": "conn4",
      +    "protocol": "kubernetes",
      +  }
      +]
      + +

      {{'CONNECTION_IMPORT.HELP_YAML_HEADER' | translate}}

      +

      {{'CONNECTION_IMPORT.HELP_YAML_DESCRIPTION' | translate}}

      +
      ---
      +  - name: conn1
      +    protocol: vnc
      +    parameters:
      +      hostname: conn1.web.com
      +    group: ROOT
      +    users:
      +    - guac user 1
      +    - guac user 2
      +    groups:
      +    - AWS EC2 Administrators
      +    attributes:
      +      guacd-encryption: none
      +  - name: conn2
      +    protocol: rdp
      +    parameters:
      +      hostname: conn2.web.com
      +    group: ROOT/Parent Group
      +    users:
      +    - guac user 1
      +    attributes:
      +      guacd-encryption: none
      +  - name: conn3
      +    protocol: ssh
      +    parameters:
      +      hostname: conn3.web.com
      +    group: ROOT/Parent Group/Child Group
      +    users:
      +    - guac user 2
      +    - guac user 3
      +  - name: conn4
      +    protocol: kubernetes
      + +
        +
      1. {{'CONNECTION_IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
      2. +
      + +
      diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html index 0e9741ede..08dbaf511 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html @@ -3,7 +3,7 @@
      {{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}} {{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}}
      diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js index dd9cc0637..7caeee526 100644 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js @@ -135,6 +135,14 @@ angular.module('index').config(['$routeProvider', '$locationProvider', resolve : { updateCurrentToken: updateCurrentToken } }) + // Connection import page + .when('/import/connection/file-format-help', { + title : 'APP.NAME', + bodyClassName : 'settings', + templateUrl : 'app/import/templates/connectionImportFileHelp.html', + resolve : { updateCurrentToken: updateCurrentToken } + }) + // Management screen .when('/settings/:dataSource?/:tab', { title : 'APP.NAME', diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 73afddb3c..06e3a4d9d 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -191,6 +191,24 @@ "HEADER": "Connection Import", + "HELP_HEADER": "Connection Import File Format", + + "HELP_FILE_TYPE_HEADER": "File Types", + "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified.", + + "HELP_CSV_HEADER": "CSV Format", + "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", + "HELP_CSV_MORE_DETAILS": "The CSV header for each row specifies the connection field. The connection group ID that the connection should be imported into may be directly specified with \"parentIdentifier\", or the path to the parent group may be specified using \"group\" as shown below. In most cases, there should be no conflict between fields, but if needed, an \" (attribute)\" or \" (parameter)\" suffix may be added to disambiguate. Lists of user or user group identifiers must be semicolon-seperated.¹", + + "HELP_JSON_HEADER": "JSON Format", + "HELP_JSON_DESCRIPTION": "A connection import JSON file is a list of connection objects. At minimum the connection name and protocol must be specified in each connection object.", + "HELP_JSON_MORE_DETAILS": "The connection group ID that the connection should be imported into may be directly specified with a \"parentIdentifier\" field, or the path to the parent group may be specified using a \"group\" field as shown below. An array of user and user group identifiers to grant access to may be specified per connection.", + + "HELP_YAML_HEADER": "YAML Format", + "HELP_YAML_DESCRIPTION": "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.", + + "HELP_SEMICOLON_FOOTNOTE": "If needed, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", + "ERROR_AMBIGUOUS_CSV_HEADER": "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", "ERROR_ARRAY_REQUIRED": From 5a2a4a79a7d45a4cc10c96e09bdd419d3938efa4 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 21 Feb 2023 23:58:15 +0000 Subject: [PATCH 157/289] GUACAMOLE-926: Roll back created users and user groups as well. --- .../importConnectionsController.js | 184 +++++++++++++----- .../directives/connectionImportFileUpload.js | 9 +- .../import/services/connectionParseService.js | 8 +- .../frontend/src/app/import/styles/help.css | 1 - .../templates/connectionImportFileHelp.html | 6 +- .../main/frontend/src/translations/en.json | 5 +- 6 files changed, 146 insertions(+), 67 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 31a49adf8..73acb1cc3 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -106,33 +106,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } - /** - * Given a successful response to an import PATCH request, make another - * request to delete every created connection in the provided request, i.e. - * clean up every connection that was created. - * - * @param {DirectoryPatchResponse} creationResponse - */ - function cleanUpConnections(creationResponse) { - - // The patches to delete - one delete per initial creation - const deletionPatches = creationResponse.patches.map(patch => - new DirectoryPatch({ - op: 'remove', - path: '/' + patch.identifier - })); - - console.log("Deletion Patches", deletionPatches); - - connectionService.patchConnections( - $routeParams.dataSource, deletionPatches) - - .then(deletionResponse => - console.log("Deletion response", deletionResponse)) - .catch(handleError); - - } - /** * Create all users and user groups mentioned in the import file that don't * already exist in the current data source. @@ -178,8 +151,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ })); return $q.all({ - createdUsers: userService.patchUsers(dataSource, userPatches), - createdGroups: userGroupService.patchUserGroups(dataSource, groupPatches) + userResponse: userService.patchUsers(dataSource, userPatches), + groupResponse: userGroupService.patchUserGroups(dataSource, groupPatches) }); }); @@ -256,22 +229,126 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ return $q.all({ ...userRequests, ...groupRequests }); } + // Given a PATCH API response, create an array of patches to delete every + // entity created in the original request that generated this response + const createDeletionPatches = creationResponse => + creationResponse.patches.map(patch => + + // Add one deletion patch per original creation patch + new DirectoryPatch({ + op: 'remove', + path: '/' + patch.identifier + })); + + /** + * Given a successful response to a connection PATCH request, make another + * request to delete every created connection in the provided request, i.e. + * clean up every connection that was created. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the connection PATCH request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpConnections(creationResponse) { + + return connectionService.patchConnections( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Given a successful response to a user PATCH request, make another + * request to delete every created user in the provided request. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the user PATCH request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpUsers(creationResponse) { + + return userService.patchUsers( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Given a successful response to a user group PATCH request, make another + * request to delete every created user group in the provided request. + * + * @param {DirectoryPatchResponse} creationResponse + * The response to the user group PATCH creation request. + * + * @returns {DirectoryPatchResponse} + * The response to the PATCH deletion request. + */ + function cleanUpUserGroups(creationResponse) { + + return userGroupService.patchUserGroups( + $routeParams.dataSource, createDeletionPatches(creationResponse)) + + // TODO: Better error handling? Make additional cleanup requests? + .catch(handleError); + + } + + /** + * Make requests to delete all connections, users, and/or groups from any + * provided PATCH API responses. If any responses are not provided, no + * cleanup will be attempted. + * + * @param {DirectoryPatchResponse} connectionResponse + * The response to the connection PATCH creation request. + * + * @param {DirectoryPatchResponse} userResponse + * The response to the user PATCH creation request. + * + * @param {DirectoryPatchResponse} userGroupResponse + * The response to the user group PATCH creation request. + * + * @returns {Object} + * An object containing PATCH deletion responses corresponding to any + * provided connection, user, and/or user group creation responses. + */ + function cleanUpAll(connectionResponse, userResponse, userGroupResponse) { + + // All cleanup requests that need to be made + const requests = {}; + + // If the connection response was provided, clean up connections + if (connectionResponse) + requests.connectionCleanup = cleanUpConnections(connectionResponse); + + // If the user response was provided, clean up users + if (userResponse) + requests.userCleanup = cleanUpUsers(userResponse); + + // If the user group response was provided, clean up user groups + if (connectionResponse) + requests.userGroupCleanup = cleanUpUserGroups(userGroupResponse); + + // Return when all cleanup is complete + return $q.all(requests); + } + /** * Process a successfully parsed import file, creating any specified * connections, creating and granting permissions to any specified users - * and user groups. - * - * TODO: - * - Do batch import of connections - * - Create all users/groups not already present - * - Grant permissions to all users/groups as defined in the import file - * - On failure: Roll back everything (maybe ask the user first): - * - Attempt to delete all created connections - * - Attempt to delete any created users / groups + * and user groups. If successful, the user will be shown a success message. + * If not, any errors will be displayed, and the user will be given ???an + * option??? to roll back any already-created entities. * * @param {ParseResult} parseResult * The result of parsing the user-supplied import file. - * */ function handleParseSuccess(parseResult) { @@ -281,21 +358,20 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // First, attempt to create the connections connectionService.patchConnections(dataSource, parseResult.patches) - .then(response => { + .then(connectionResponse => { // If connection creation is successful, create users and groups - createUsersAndGroups(parseResult).then(() => { + createUsersAndGroups(parseResult).then( + ({userResponse, groupResponse}) => - grantConnectionPermissions(parseResult, response).then(results => { - console.log("permission requests", results); + grantConnectionPermissions(parseResult, connectionResponse) + .then(() => - // TODON'T: Delete connections so we can test over and over - cleanUpConnections(response); + // TODON'T: Delete the stuff so we can test over and over + cleanUpAll(connectionResponse, userResponse, groupResponse) + .then(resetUploadState) - resetUploadState(); - }) - - }); + )); }); } @@ -315,7 +391,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ console.error(error); $scope.error = error; - } + }; /** * Clear the current displayed error. @@ -356,14 +432,16 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // have already have filtered out any invalid file types else { handleError(new ParseError({ - message: 'Invalid file type: ' + type, + message: 'Invalid file type: ' + mimeType, key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', - variables: { TYPE: type } + variables: { TYPE: mimeType } })); return; } // Make the call to process the data into a series of patches + // TODO: Check if there's errors, and if so, display those rather than + // just YOLOing a create call processDataCallback(data) // Send the data off to be imported if parsing is successful @@ -476,6 +554,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Read all the data into memory $scope.fileReader.readAsBinaryString(file); - } + }; }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js index d988e7c7d..4e2c76b24 100644 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -19,17 +19,16 @@ /* global _ */ -/** - * A directive that allows for file upload, either through drag-and-drop or - * a file browser. - */ - /** * All legal import file types. Any file not belonging to one of these types * must be rejected. */ const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; +/** + * A directive that allows for file upload, either through drag-and-drop or + * a file browser. + */ angular.module('import').directive('connectionImportFileUpload', [ function connectionImportFileUpload() { diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index 4b9f50c2d..cf15a374e 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -142,7 +142,7 @@ angular.module('import').factory('connectionParseService', // If there's no group to translate, do nothing if (!connection.group) - return; + return connection; // If both are specified, the parent group is ambigious if (connection.parentIdentifier) @@ -333,8 +333,9 @@ angular.module('import').factory('connectionParseService', service.parseYAML = function parseYAML(yamlData) { // Parse from YAML into a javascript array + let connectionData; try { - const connectionData = parseYAMLData(yamlData); + connectionData = parseYAMLData(yamlData); } // If the YAML parser throws an error, reject with that error. No @@ -366,8 +367,9 @@ angular.module('import').factory('connectionParseService', service.parseJSON = function parseJSON(jsonData) { // Parse from JSON into a javascript array + let connectionData; try { - const connectionData = JSON.parse(jsonData); + connectionData = JSON.parse(jsonData); } // If the JSON parse attempt throws an error, reject with that error. diff --git a/guacamole/src/main/frontend/src/app/import/styles/help.css b/guacamole/src/main/frontend/src/app/import/styles/help.css index e78acea4a..a73b43afa 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/help.css +++ b/guacamole/src/main/frontend/src/app/import/styles/help.css @@ -31,7 +31,6 @@ .import.help h2 { - padding-top: 0px; padding-bottom: 0px; } diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html index bd7f21845..e6063e89f 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -43,11 +43,11 @@ conn4,kubernetes,,,,, "protocol": "ssh", "parameters": { "hostname": "conn3.web.com" }, "group": "ROOT/Parent Group/Child Group", - "users": [ "guac user 2", "guac user 3" ], + "users": [ "guac user 2", "guac user 3" ] }, { "name": "conn4", - "protocol": "kubernetes", + "protocol": "kubernetes" } ] @@ -63,7 +63,7 @@ conn4,kubernetes,,,,, - guac user 1 - guac user 2 groups: - - AWS EC2 Administrators + - Connection 1 Users attributes: guacd-encryption: none - name: conn2 diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 06e3a4d9d..0233f8bb6 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -194,7 +194,8 @@ "HELP_HEADER": "Connection Import File Format", "HELP_FILE_TYPE_HEADER": "File Types", - "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified.", + "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created.", + "HELP_CSV_HEADER": "CSV Format", "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", @@ -207,7 +208,7 @@ "HELP_YAML_HEADER": "YAML Format", "HELP_YAML_DESCRIPTION": "A connection import YAML file is a list of connection objects with exactly the same structure as the JSON format.", - "HELP_SEMICOLON_FOOTNOTE": "If needed, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", + "HELP_SEMICOLON_FOOTNOTE": "If present, semicolons can be escaped with a backslash, e.g. \"first\\\\;last\"", "ERROR_AMBIGUOUS_CSV_HEADER": "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", From 322adbc294c2189019e84a278017fdf85d309658 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 23 Feb 2023 19:44:46 +0000 Subject: [PATCH 158/289] GUACAMOLE-926: Display connection-specific errors to the user. --- .../importConnectionsController.js | 57 ++-- .../directives/connectionImportErrors.js | 248 ++++++++++++++++++ .../directives/connectionImportFileUpload.js | 4 +- .../frontend/src/app/import/importModule.js | 2 +- .../import/services/connectionCSVService.js | 12 +- .../import/services/connectionParseService.js | 8 +- .../frontend/src/app/import/styles/import.css | 17 +- .../import/templates/connectionErrors.html | 45 ++++ .../import/templates/connectionImport.html | 9 +- .../templates/connectionImportFileHelp.html | 24 +- .../templates/connectionImportFileUpload.html | 8 +- .../src/app/import/types/DisplayErrorList.js | 83 ++++++ .../src/app/import/types/ImportConnection.js | 2 +- .../app/import/types/ImportConnectionError.js | 108 ++++++++ .../app/rest/services/connectionService.js | 4 +- .../main/frontend/src/app/rest/types/Error.js | 9 + .../main/frontend/src/translations/en.json | 16 +- 17 files changed, 598 insertions(+), 58 deletions(-) create mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js create mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html create mode 100644 guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js create mode 100644 guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 73acb1cc3..ec1227e95 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -49,6 +49,21 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ $scope.error = null; + /** + * The result of parsing the current upload, if successful. + * + * @type {ParseResult} + */ + $scope.parseResult = null; + + /** + * The failure associated with the current attempt to create connections + * through the API, if any. + * + * @type {Error} + */ + $scope.patchFailure = null;; + /** * True if the file is fully uploaded and ready to be processed, or false * otherwise. @@ -100,6 +115,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; + $scope.parseResult = null; + $scope.patchFailure = null; // Broadcast an event to clear the file upload UI $scope.$broadcast('clearFile'); @@ -254,10 +271,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpConnections(creationResponse) { return connectionService.patchConnections( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -274,10 +288,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpUsers(creationResponse) { return userService.patchUsers( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -294,10 +305,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ function cleanUpUserGroups(creationResponse) { return userGroupService.patchUserGroups( - $routeParams.dataSource, createDeletionPatches(creationResponse)) - - // TODO: Better error handling? Make additional cleanup requests? - .catch(handleError); + $routeParams.dataSource, createDeletionPatches(creationResponse)); } @@ -352,6 +360,15 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ function handleParseSuccess(parseResult) { + $scope.processing = false; + $scope.parseResult = parseResult; + + // If errors were encounted during file parsing, abort further + // processing - the user will have a chance to fix the errors and try + // again + if (parseResult.hasErrors) + return; + const dataSource = $routeParams.dataSource; console.log("parseResult", parseResult); @@ -372,7 +389,12 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ .then(resetUploadState) )); - }); + }) + + // If an error occured when the call to create the connections was made, + // skip any further processing - the user will have a chance to fix the + // problems and try again + .catch(patchFailure => { $scope.patchFailure = patchFailure; }); } /** @@ -433,7 +455,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ else { handleError(new ParseError({ message: 'Invalid file type: ' + mimeType, - key: 'CONNECTION_IMPORT.INVALID_FILE_TYPE', + key: 'IMPORT.INVALID_FILE_TYPE', variables: { TYPE: mimeType } })); return; @@ -459,8 +481,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ /** * @return {Boolean} - * True if import should be disabled, or false if cancellation - * should be allowed. + * True if import should be disabled, or false if import should be + * allowed. */ $scope.importDisabled = () => @@ -471,7 +493,8 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.processing; /** - * Cancel any in-progress upload, or clear any uploaded-but + * Cancel any in-progress upload, or clear any uploaded-but-errored-out + * batch. */ $scope.cancel = function() { diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js new file mode 100644 index 000000000..dcf42695a --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js @@ -0,0 +1,248 @@ +/* + * 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. + */ + +/* global _ */ + +/** + * A directive that displays errors that occured during parsing of a connection + * import file, or errors that were returned from the API during the connection + * batch creation attempt. + */ +angular.module('import').directive('connectionImportErrors', [ + function connectionImportErrors() { + + const directive = { + restrict: 'E', + replace: true, + templateUrl: 'app/import/templates/connectionErrors.html', + scope: { + + /** + * The result of parsing the import file. Any errors in this file + * will be displayed to the user. + * + * @type ParseResult + */ + parseResult : '=', + + /** + * The error associated with an attempt to batch create the + * connections represented by the ParseResult, if the ParseResult + * had no errors. If the provided ParseResult has errors, no request + * should have been made, and any provided patch error will be + * ignored. + * + * @type Error + */ + patchFailure : '=', + + } + }; + + directive.controller = ['$scope', '$injector', + function connectionImportErrorsController($scope, $injector) { + + // Required types + const DisplayErrorList = $injector.get('DisplayErrorList'); + const ImportConnectionError = $injector.get('ImportConnectionError'); + const ParseError = $injector.get('ParseError'); + const SortOrder = $injector.get('SortOrder'); + + // Required services + const $q = $injector.get('$q'); + const $translate = $injector.get('$translate'); + + // There are errors to display if the parse result generated errors, or + // if the patch request failed + $scope.hasErrors = () => + !!_.get($scope, 'parseResult.hasErrors') || !!$scope.patchFailure; + + /** + * All connections with their associated errors for display. These may + * be either parsing failures, or errors returned from the API. Both + * error types will be adapted to a common display format, though the + * error types will never be mixed, because no REST request should ever + * be made if there are client-side parse errors. + * + * @type {ImportConnectionError[]} + */ + $scope.connectionErrors = []; + + /** + * SortOrder instance which maintains the sort order of the visible + * connection errors. + * + * @type SortOrder + */ + $scope.errorOrder = new SortOrder([ + 'rowNumber', + 'name', + 'protocol', + 'errors', + ]); + + /** + * Array of all connection error properties that are filterable. + * + * @type String[] + */ + $scope.filteredErrorProperties = [ + 'rowNumber', + 'name', + 'protocol', + 'errors', + ]; + + /** + * Generate a ImportConnectionError representing any errors associated + * with the row at the given index within the given parse result. + * + * @param {ParseResult} parseResult + * The result of parsing the connection import file. + * + * @param {Integer} index + * The current row within the import file, 0-indexed. + * + * @returns {ImportConnectionError} + * The connection error object associated with the given row in the + * given parse result. + */ + const generateConnectionError = (parseResult, index) => { + + // Get the patch associated with the current row + const patch = parseResult.patches[index]; + + // The value of a patch is just the Connection object + const connection = patch.value; + + return new ImportConnectionError({ + + // Add 1 to the index to get the position in the file + rowNumber: index + 1, + + // Basic connection information - name and protocol. + name: connection.name, + protocol: connection.protocol, + + // The group and parent identifiers, if any are set. Include + // both since these could be a potential source of conflict. + // TODO: Should we _really_ have both of these here? + group: connection.group, + parentIdentifier: connection.parentIdentifier, + + // Get the list of user and group identifiers from the parse + // result. There should one entry in each of these lists for + // each patch. + users: parseResult.users[index], + groups: parseResult.groups[index], + + // The human-readable error messages + errors: new DisplayErrorList( + [ ...(parseResult.errors[index] || []) ]) + }); + }; + + // If a new connection patch failure is seen, update the display list + $scope.$watch('patchFailure', async function patchFailureChanged(patchFailure) { + + const { parseResult } = $scope; + + // Do not attempt to process anything before the data has loaded + if (!patchFailure || !parseResult) + return; + + // Set up the list of connection errors based on the existing parse + // result, with error messages fetched from the patch failure + $scope.connectionErrors = parseResult.patches.map( + (patch, index) => { + + // Generate a connection error for display + const connectionError = generateConnectionError(parseResult, index); + + // Set the error from the PATCH request, if there is one + // TODO: These generally aren't translated from the backend - + // should we even bother trying to translate them? + const error = _.get(patchFailure, ['patches', index, 'error']); + if (error) + connectionError.errors = new DisplayErrorList([error]); + + return connectionError; + }); + }); + + // If a new parse result with errors is seen, update the display list + $scope.$watch('parseResult', async function parseResultChanged(parseResult) { + + // Do not process if there are no errors in the provided result + if (!parseResult || !parseResult.hasErrors) + return; + + // All promises from all translation requests. The scope will not be + // updated until all translations are ready. + const translationPromises = []; + + // The parse result should only be updated on a fresh file import; + // therefore it should be safe to skip checking the patch errors + // entirely - if set, they will be from the previous file and no + // longer relevant. + + // Set up the list of connection errors based on the updated parse + // result + const connectionErrors = parseResult.patches.map( + (patch, index) => { + + // Generate a connection error for display + const connectionError = generateConnectionError(parseResult, index); + + // Go through the errors and check if any are translateable + connectionError.errors.getArray().forEach( + (error, errorIndex) => { + + // If this error is a ParseError, it can be translated. + // NOTE: Generally one would translate error messages in the + // template, but in this case, the connection errors need to + // be raw strings in order to enable sorting and filtering. + if (error instanceof ParseError) + + // Fetch the translation and update it when it's ready + translationPromises.push($translate( + error.key, error.variables) + .then(translatedError => { + connectionError.errors.getArray()[errorIndex] = translatedError; + })); + + }); + + return connectionError; + + }); + + // Once all the translations have been completed, update the + // connectionErrors all in one go, to ensure no excessive reloading + $q.all(translationPromises).then(() => { + $scope.connectionErrors = connectionErrors; + }); + + }); + + }]; + + return directive; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js index 4e2c76b24..196e8f48f 100644 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js @@ -170,7 +170,7 @@ angular.module('import').directive('connectionImportFileUpload', [ // If the provided file is not one of the supported types, // display an error and abort processing - setError('CONNECTION_IMPORT.ERROR_INVALID_FILE_TYPE', + setError('IMPORT.ERROR_INVALID_FILE_TYPE', { TYPE: mimeType }); return; } @@ -204,7 +204,7 @@ angular.module('import').directive('connectionImportFileUpload', [ // If more than one file was provided, print an error explaining // that only a single file is allowed and abort processing - setError('CONNECTION_IMPORT.ERROR_FILE_SINGLE_ONLY'); + setError('IMPORT.ERROR_FILE_SINGLE_ONLY'); return; } diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js index 6480d62fc..46e5fa157 100644 --- a/guacamole/src/main/frontend/src/app/import/importModule.js +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -21,4 +21,4 @@ * The module for code supporting importing user-supplied files. Currently, only * connection import is supported. */ -angular.module('import', ['rest']); +angular.module('import', ['rest', 'list']); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js index 492b9388b..389749555 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js @@ -238,7 +238,7 @@ angular.module('import').factory('connectionCSVService', deferred.reject(new ParseError({ message: 'Duplicate CSV Header: ' + header, translatableMessage: new TranslatableMessage({ - key: 'CONNECTION_IMPORT.ERROR_DUPLICATE_CSV_HEADER', + key: 'IMPORT.ERROR_DUPLICATE_CSV_HEADER', variables: { HEADER: header } }) })); @@ -342,7 +342,7 @@ angular.module('import').factory('connectionCSVService', if (isAttribute && isParameter) throw new ParseError({ message: 'Ambiguous CSV Header: ' + header, - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', + key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', variables: { HEADER: header } }); @@ -350,7 +350,7 @@ angular.module('import').factory('connectionCSVService', else if (!isAttribute && !isParameter) throw new ParseError({ message: 'Invalid CSV Header: ' + header, - key: 'CONNECTION_IMPORT.ERROR_INVALID_CSV_HEADER', + key: 'IMPORT.ERROR_INVALID_CSV_HEADER', variables: { HEADER: header } }); @@ -372,21 +372,21 @@ angular.module('import').factory('connectionCSVService', if (!nameGetter) return deferred.reject(new ParseError({ message: 'The connection name must be provided', - key: 'CONNECTION_IMPORT.ERROR_REQUIRED_NAME' + key: 'IMPORT.ERROR_REQUIRED_NAME' })); // Fail if the protocol wasn't provided if (!protocolGetter) return deferred.reject(new ParseError({ message: 'The connection protocol must be provided', - key: 'CONNECTION_IMPORT.ERROR_REQUIRED_PROTOCOL' + key: 'IMPORT.ERROR_REQUIRED_PROTOCOL' })); // If both are specified, the parent group is ambigious if (parentIdentifierGetter && groupGetter) throw new ParseError({ message: 'Only one of group or parentIdentifier can be set', - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' }); // The function to transform a CSV row into a connection object diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js index cf15a374e..dd9f49593 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js @@ -60,7 +60,7 @@ angular.module('import').factory('connectionParseService', if (!(parsedData instanceof Array)) return new ParseError({ message: 'Import data must be a list of connections', - key: 'CONNECTION_IMPORT.ERROR_ARRAY_REQUIRED' + key: 'IMPORT.ERROR_ARRAY_REQUIRED' }); // Make sure that the connection list is not empty - contains at least @@ -68,7 +68,7 @@ angular.module('import').factory('connectionParseService', if (!parsedData.length) return new ParseError({ message: 'The provided file is empty', - key: 'CONNECTION_IMPORT.ERROR_EMPTY_FILE' + key: 'IMPORT.ERROR_EMPTY_FILE' }); } @@ -148,7 +148,7 @@ angular.module('import').factory('connectionParseService', if (connection.parentIdentifier) throw new ParseError({ message: 'Only one of group or parentIdentifier can be set', - key: 'CONNECTION_IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' }); // Look up the parent identifier for the specified group path @@ -158,7 +158,7 @@ angular.module('import').factory('connectionParseService', if (!identifier) throw new ParseError({ message: 'No group found named: ' + connection.group, - key: 'CONNECTION_IMPORT.ERROR_INVALID_GROUP', + key: 'IMPORT.ERROR_INVALID_GROUP', variables: { GROUP: connection.group } }); diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 5addb1933..8f038e4d1 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -27,5 +27,18 @@ display: flex; gap: 10px; justify-content: center; - -} \ No newline at end of file + +} + + +.import .errors table { + width: 100%; +} + +.import .errors .error-message { + color: red; +} + +.import .errors .error-message ul { + margin: 0px; +} diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html b/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html new file mode 100644 index 000000000..eee23c260 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html @@ -0,0 +1,45 @@ +
      + + + + + + + + + + + + + + + + + + + + + + +
      + {{'IMPORT.TABLE_HEADER_ROW_NUMBER' | translate}} + + {{'IMPORT.TABLE_HEADER_NAME' | translate}} + + {{'IMPORT.TABLE_HEADER_PROTOCOL' | translate}} + + {{'IMPORT.TABLE_HEADER_ERRORS' | translate}} +
      {{error.rowNumber}}{{error.name}}{{error.protocol}} +
        +
      • + {{ message }} +
      • +
      +
      + + + +
      diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index 881cb2429..d0f9fd537 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -1,7 +1,7 @@
      -

      {{'CONNECTION_IMPORT.HEADER' | translate}}

      +

      {{'IMPORT.HEADER' | translate}}

      @@ -10,11 +10,11 @@
      @@ -29,5 +29,8 @@ {{error.message}}

      + + +
      diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html index e6063e89f..8e88d235a 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html @@ -1,25 +1,25 @@
      -

      {{'CONNECTION_IMPORT.HELP_HEADER' | translate}}

      +

      {{'IMPORT.HELP_HEADER' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

      +

      {{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

      +

      {{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_CSV_HEADER' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_CSV_DESCRIPTION' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

      +

      {{'IMPORT.HELP_CSV_HEADER' | translate}}

      +

      {{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}

      +

      {{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

      name,protocol,hostname,group,users,groups,guacd-encryption (attribute)
       conn1,vnc,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none
       conn2,rdp,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl
       conn3,ssh,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,
       conn4,kubernetes,,,,,
      -

      {{'CONNECTION_IMPORT.HELP_JSON_HEADER' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_JSON_DESCRIPTION' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

      +

      {{'IMPORT.HELP_JSON_HEADER' | translate}}

      +

      {{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}

      +

      {{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

      [
         {
           "name": "conn1",
      @@ -51,8 +51,8 @@ conn4,kubernetes,,,,,
      } ] -

      {{'CONNECTION_IMPORT.HELP_YAML_HEADER' | translate}}

      -

      {{'CONNECTION_IMPORT.HELP_YAML_DESCRIPTION' | translate}}

      +

      {{'IMPORT.HELP_YAML_HEADER' | translate}}

      +

      {{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}

      ---
         - name: conn1
           protocol: vnc
      @@ -87,7 +87,7 @@ conn4,kubernetes,,,,,
      protocol: kubernetes
        -
      1. {{'CONNECTION_IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
      2. +
      3. {{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
      diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html index 08dbaf511..188567c00 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html @@ -1,20 +1,20 @@
      - {{'CONNECTION_IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'IMPORT.UPLOAD_FILE_TYPES' | translate}} {{'CONNECTION_IMPORT.UPLOAD_HELP_LINK' | translate}} + class="file-help-link">{{'IMPORT.UPLOAD_HELP_LINK' | translate}}
      -
      {{'CONNECTION_IMPORT.UPLOAD_DROP_TITLE' | translate}}
      +
      {{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
      - {{'CONNECTION_IMPORT.UPLOAD_BROWSE_LINK' | translate}} + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}}
      {{fileName}}
      diff --git a/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js b/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js new file mode 100644 index 000000000..d775c5734 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js @@ -0,0 +1,83 @@ +/* + * 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. + */ + +/** + * Service which defines the DisplayErrorList class. + */ +angular.module('import').factory('DisplayErrorList', [ + function defineDisplayErrorList() { + + /** + * A list of human-readable error messages, intended to be usable in a + * sortable / filterable table. + * + * @constructor + * @param {String[]} messages + * The error messages that should be prepared for display. + */ + const DisplayErrorList = function DisplayErrorList(messages) { + + // Use empty message list by default + this.messages = messages || []; + + // The single String message composed of all messages concatenated + // together. This will be used for filtering / sorting, and should only + // be calculated once. + this.cachedMessage = null; + + }; + + /** + * Return a sortable / filterable representation of all the error messages + * wrapped by this DisplayErrorList. + * + * NOTE: Once this method is called, any changes to the underlying array + * will have no effect. This is to ensure that repeated calls to toString() + * by sorting / filtering UI code will not regenerate the concatenated + * message every time. + * + * @returns {String} + * A sortable / filterable representation of the error messages wrapped + * by this DisplayErrorList + */ + DisplayErrorList.prototype.toString = function messageListToString() { + + // Generate the concatenated message if not already generated + if (!this.concatenatedMessage) + this.concatenatedMessage = this.messages.join(' '); + + return this.concatenatedMessage; + + } + + /** + * Return the underlying array containing the raw error messages, wrapped + * by this DisplayErrorList. + * + * @returns {String[]} + * The underlying array containing the raw error messages, wrapped by + * this DisplayErrorList + */ + DisplayErrorList.prototype.getArray = function getUnderlyingArray() { + return this.messages; + } + + return DisplayErrorList; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js index 9f336ef77..5639ba1c6 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js @@ -32,7 +32,7 @@ angular.module('import').factory('ImportConnection', [ * The object whose properties should be copied within the new * Connection. */ - var ImportConnection = function ImportConnection(template) { + const ImportConnection = function ImportConnection(template) { // Use empty object by default template = template || {}; diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js new file mode 100644 index 000000000..4d4fd3661 --- /dev/null +++ b/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js @@ -0,0 +1,108 @@ +/* + * 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. + */ + +/** + * Service which defines the ImportConnectionError class. + */ +angular.module('import').factory('ImportConnectionError', ['$injector', + function defineImportConnectionError($injector) { + + // Required types + const DisplayErrorList = $injector.get('DisplayErrorList'); + + /** + * A representation of a connection to be imported, as parsed from an + * user-supplied import file. + * + * @constructor + * @param {ImportConnection|Object} [template={}] + * The object whose properties should be copied within the new + * Connection. + */ + const ImportConnectionError = function ImportConnectionError(template) { + + // Use empty object by default + template = template || {}; + + /** + * The row number within the original connection import file for this + * connection. This should be 1-indexed. + */ + this.rowNumber = template.rowNumber; + + /** + * The unique identifier of the connection group that contains this + * connection. + * + * @type String + */ + this.parentIdentifier = template.parentIdentifier; + + /** + * The path to the connection group that contains this connection, + * written as e.g. "ROOT/parent/child/group". + * + * @type String + */ + this.group = template.group; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + * + * @type String + */ + this.name = template.name; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + * + * @type String + */ + this.protocol = template.protocol; + + /** + * The identifiers of all users who should be granted read access to + * this connection. + * + * @type String[] + */ + this.users = template.users || []; + + /** + * The identifiers of all user groups who should be granted read access + * to this connection. + * + * @type String[] + */ + this.groups = template.groups || []; + + /** + * The error messages associated with this particular connection, if any. + * + * @type ImportConnectionError + */ + this.errors = template.errors || new DisplayErrorList(); + + }; + + return ImportConnectionError; + +}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js index 5c1451ec2..c46874ea4 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js @@ -24,9 +24,11 @@ angular.module('rest').factory('connectionService', ['$injector', function connectionService($injector) { // Required services - var requestService = $injector.get('requestService'); var authenticationService = $injector.get('authenticationService'); var cacheService = $injector.get('cacheService'); + + // Required types + const Error = $injector.get('Error'); var service = {}; diff --git a/guacamole/src/main/frontend/src/app/rest/types/Error.js b/guacamole/src/main/frontend/src/app/rest/types/Error.js index 47f9cf770..74b28ebae 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/Error.js +++ b/guacamole/src/main/frontend/src/app/rest/types/Error.js @@ -78,6 +78,15 @@ angular.module('rest').factory('Error', [function defineError() { */ this.expected = template.expected; + /** + * The outcome for each patch that was submitted as part of the request + * that generated this error, if the request was a directory PATCH + * request. In all other cases, this will be null. + * + * @type DirectoryPatchOutcome[] + */ + this.patches = template.patches || null; + }; /** diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index 0233f8bb6..c5292b80f 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -184,11 +184,13 @@ }, - "CONNECTION_IMPORT": { + "IMPORT": { "BUTTON_CANCEL": "Cancel", "BUTTON_IMPORT": "Import Connections", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "HEADER": "Connection Import", "HELP_HEADER": "Connection Import File Format", @@ -196,7 +198,6 @@ "HELP_FILE_TYPE_HEADER": "File Types", "HELP_FILE_TYPE_DESCRIPTION" : "Three file types are supported for connection import: CSV, JSON, and YAML. The same data may be specified by each file type. This must include the connection name and protocol. Optionally, a connection group location, a list of users and/or user groups to grant access, connection parameters, or connection protocols may also be specified. Any users or user groups that do not exist in the current data source will be automatically created.", - "HELP_CSV_HEADER": "CSV Format", "HELP_CSV_DESCRIPTION": "A connection import CSV file has one connection record per row. Each column will specify a connection field. At minimum the connection name and protocol must be specified.", "HELP_CSV_MORE_DETAILS": "The CSV header for each row specifies the connection field. The connection group ID that the connection should be imported into may be directly specified with \"parentIdentifier\", or the path to the parent group may be specified using \"group\" as shown below. In most cases, there should be no conflict between fields, but if needed, an \" (attribute)\" or \" (parameter)\" suffix may be added to disambiguate. Lists of user or user group identifiers must be semicolon-seperated.¹", @@ -234,12 +235,17 @@ "ERROR_REQUIRED_NAME": "No connection name found in the provided file", + "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time", + + "TABLE_HEADER_NAME" : "Name", + "TABLE_HEADER_PROTOCOL" : "Protocol", + "TABLE_HEADER_ERRORS" : "Errors", + "TABLE_HEADER_ROW_NUMBER": "Row Number", + "UPLOAD_FILE_TYPES": "CSV, JSON, or YAML", "UPLOAD_HELP_LINK": "View Format Tips", "UPLOAD_DROP_TITLE": "Drop a File Here", - "UPLOAD_BROWSE_LINK": "Browse for File", - - "ERROR_FILE_SINGLE_ONLY": "Please upload only a single file at a time" + "UPLOAD_BROWSE_LINK": "Browse for File" }, From 2ce7876f26845502a2d5b73f616ab7b8b546f867 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 3 Mar 2023 02:38:47 +0000 Subject: [PATCH 159/289] GUACAMOLE-926: Migrate upload directive into import page controller since they're so tangled up together to make it not worth seperating them. --- .../importConnectionsController.js | 178 ++++++++++++- .../directives/connectionImportFileUpload.js | 252 ------------------ .../src/app/import/styles/file-upload.css | 119 --------- .../frontend/src/app/import/styles/import.css | 101 +++++++ .../import/templates/connectionImport.html | 26 +- .../templates/connectionImportFileUpload.html | 30 --- 6 files changed, 300 insertions(+), 406 deletions(-) delete mode 100644 guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js delete mode 100644 guacamole/src/main/frontend/src/app/import/styles/file-upload.css delete mode 100644 guacamole/src/main/frontend/src/app/import/templates/connectionImportFileUpload.html diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index ec1227e95..14da850f9 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -25,9 +25,14 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$injector', function importConnectionsController($scope, $injector) { + // The file types supported for connection import + const LEGAL_FILE_TYPES = ['csv', 'json', 'yaml']; + // Required services + const $document = $injector.get('$document'); const $q = $injector.get('$q'); const $routeParams = $injector.get('$routeParams'); + const $timeout = $injector.get('$timeout'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); const permissionService = $injector.get('permissionService'); @@ -112,11 +117,13 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.aborted = false; $scope.dataReady = false; $scope.processing = false; + $scope.error = null; $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; $scope.parseResult = null; $scope.patchFailure = null; + $scope.fileName = null; // Broadcast an event to clear the file upload UI $scope.$broadcast('clearFile'); @@ -410,7 +417,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ resetUploadState(); // Set the error for display - console.error(error); $scope.error = error; }; @@ -462,8 +468,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } // Make the call to process the data into a series of patches - // TODO: Check if there's errors, and if so, display those rather than - // just YOLOing a create call processDataCallback(data) // Send the data off to be imported if parsing is successful @@ -537,7 +541,30 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ * @argument {File} file * The file to upload onto the scope for further processing. */ - $scope.handleFile = function(file) { + const handleFile = file => { + + // Clear any error from a previous attempted file upload + clearError(); + + // The MIME type of the provided file + const mimeType = file.type; + + // Check if the mimetype ends with one of the supported types, + // e.g. "application/json" or "text/csv" + if (_.every(LEGAL_FILE_TYPES.map( + type => !mimeType.endsWith(type)))) { + + // If the provided file is not one of the supported types, + // display an error and abort processing + handleError(new ParseError({ + message: "Invalid file type: " + type, + key: 'IMPORT.ERROR_INVALID_FILE_TYPE', + variables: { TYPE: mimeType } + })); + return; + } + + $scope.fileName = file.name; // Clear any error message from the previous upload attempt clearError(); @@ -578,5 +605,148 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // Read all the data into memory $scope.fileReader.readAsBinaryString(file); }; + + /** + * Whether a drag/drop operation is currently in progress (the user has + * dragged a file over the Guacamole connection but has not yet + * dropped it). + * + * @type boolean + */ + $scope.dropPending = false; + + /** + * The name of the file that's currently being uploaded, or has yet to + * be imported, if any. + */ + $scope.fileName = null; + + /** + * The container for the file upload UI. + * + * @type Element + * + */ + const uploadContainer = angular.element( + $document.find('.file-upload-container')); + + /** + * The location where files can be dragged-and-dropped to. + * + * @type Element + */ + const dropTarget = uploadContainer.find('.drop-target'); + + /** + * Displays a visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the in-progress drag/drop operation. + */ + const notifyDragStart = function notifyDragStart(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = true; + }); + + }; + + /** + * Removes the visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param {Event} e + * The event related to the end of the former drag/drop operation. + */ + const notifyDragEnd = function notifyDragEnd(e) { + + e.preventDefault(); + e.stopPropagation(); + + $scope.$apply(() => { + $scope.dropPending = false; + }); + + }; + + // Add listeners to the drop target to ensure that the visual state + // stays up to date + dropTarget.on('dragenter', notifyDragStart); + dropTarget.on('dragover', notifyDragStart); + dropTarget.on('dragleave', notifyDragEnd); + + /** + * Drop target event listener that will be invoked if the user drops + * anything onto the drop target. If a valid file is provided, the + * onFile callback provided to this directive will be called; otherwise + * an error will be displayed, if appropriate. + * + * @param {Event} e + * The drop event that triggered this handler. + */ + dropTarget.on('drop', e => { + + notifyDragEnd(e); + + const files = e.originalEvent.dataTransfer.files; + + // Ignore any non-files that are dragged into the drop area + if (files.length < 1) + return; + + if (files.length >= 2) { + + // If more than one file was provided, print an error explaining + // that only a single file is allowed and abort processing + handleError(new ParseError({ + message: 'Only a single file may be imported at once', + key: 'IMPORT.ERROR_FILE_SINGLE_ONLY' + })); + return; + } + + handleFile(files[0]); + + }); + + /** + * The hidden file input used to create a file browser. + * + * @type Element + */ + const fileUploadInput = uploadContainer.find('.file-upload-input'); + + /** + * A function that will click on the hidden file input to open a file + * browser to allow the user to select a file for upload. + */ + $scope.openFileBrowser = () => + $timeout(() => fileUploadInput.click(), 0, false); + + /** + * A handler that will be invoked when a user selectes a file in the + * file browser. After some error checking, the file will be passed to + * the onFile callback provided to this directive. + * + * @param {Event} e + * The event that was triggered when the user selected a file in + * their file browser. + */ + fileUploadInput.on('change', e => { + + // Process the uploaded file + handleFile(e.target.files[0]); + + // Clear the value to ensure that the change event will be fired + // if the user selects the same file again + fileUploadInput.value = null; + + }); }]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js deleted file mode 100644 index 196e8f48f..000000000 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportFileUpload.js +++ /dev/null @@ -1,252 +0,0 @@ -/* - * 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. - */ - -/* global _ */ - -/** - * All legal import file types. Any file not belonging to one of these types - * must be rejected. - */ -const LEGAL_FILE_TYPES = ["csv", "json", "yaml"]; - -/** - * A directive that allows for file upload, either through drag-and-drop or - * a file browser. - */ -angular.module('import').directive('connectionImportFileUpload', [ - function connectionImportFileUpload() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/import/templates/connectionImportFileUpload.html', - scope: { - - /** - * The function to invoke when a file is provided to the file upload - * UI, either by dragging and dropping, or by navigating using the - * file browser. The function will be called with 2 arguments - the - * mime type, and the raw string contents of the file. - * - * @type function - */ - onFile : '&', - } - }; - - directive.controller = ['$scope', '$injector', '$element', - function fileUploadController($scope, $injector, $element) { - - // Required services - const $timeout = $injector.get('$timeout'); - - /** - * Whether a drag/drop operation is currently in progress (the user has - * dragged a file over the Guacamole connection but has not yet - * dropped it). - * - * @type boolean - */ - $scope.dropPending = false; - - /** - * The error associated with the file upload, if any. An object of the - * form { key, variables }, or null if no error has occured. - */ - $scope.error = null; - - /** - * The name of the file that's currently being uploaded, or has yet to - * be imported, if any. - */ - $scope.fileName = null; - - // Clear the file if instructed to do so by the parent - $scope.$on('clearFile', () => delete $scope.fileName); - - /** - * Clear any displayed error message. - */ - const clearError = () => $scope.error = null; - - /** - * Set an error for display using the provided translation key and - * translation variables. - * - * @param {String} key - * The translation key. - * - * @param {Object.} variables - * The variables to subsitute into the message, if any. - */ - const setError = (key, variables) => $scope.error = { key, variables }; - - /** - * The location where files can be dragged-and-dropped to. - * - * @type Element - */ - const dropTarget = $element.find('.drop-target')[0]; - - /** - * Displays a visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the in-progress drag/drop operation. - */ - const notifyDragStart = function notifyDragStart(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = true; - }); - - }; - - /** - * Removes the visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the end of the former drag/drop operation. - */ - const notifyDragEnd = function notifyDragEnd(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = false; - }); - - }; - - // Add listeners to the drop target to ensure that the visual state - // stays up to date - dropTarget.addEventListener('dragenter', notifyDragStart, false); - dropTarget.addEventListener('dragover', notifyDragStart, false); - dropTarget.addEventListener('dragleave', notifyDragEnd, false); - - /** - * Given a user-supplied file, validate that the file type is correct, - * and invoke the onFile callback provided to this directive if so. - * - * @param {File} file - * The user-supplied file. - */ - function handleFile(file) { - - // Clear any error from a previous attempted file upload - clearError(); - - // The MIME type of the provided file - const mimeType = file.type; - - // Check if the mimetype ends with one of the supported types, - // e.g. "application/json" or "text/csv" - if (_.every(LEGAL_FILE_TYPES.map( - type => !mimeType.endsWith(type)))) { - - // If the provided file is not one of the supported types, - // display an error and abort processing - setError('IMPORT.ERROR_INVALID_FILE_TYPE', - { TYPE: mimeType }); - return; - } - - $scope.fileName = file.name; - - // Invoke the provided file callback using the file - $scope.onFile({ file }); - } - - /** - * Drop target event listener that will be invoked if the user drops - * anything onto the drop target. If a valid file is provided, the - * onFile callback provided to this directive will be called; otherwise - * an error will be displayed, if appropriate. - * - * @param {Event} e - * The drop event that triggered this handler. - */ - dropTarget.addEventListener('drop', function(e) { - - notifyDragEnd(e); - - const files = e.dataTransfer.files; - - // Ignore any non-files that are dragged into the drop area - if (files.length < 1) - return; - - if (files.length > 2) { - - // If more than one file was provided, print an error explaining - // that only a single file is allowed and abort processing - setError('IMPORT.ERROR_FILE_SINGLE_ONLY'); - return; - } - - handleFile(files[0]); - - }, false); - - /** - * The hidden file input used to create a file browser. - * - * @type Element - */ - const fileUploadInput = $element.find('.file-upload-input')[0]; - - /** - * A function that will click on the hidden file input to open a file - * browser to allow the user to select a file for upload. - */ - $scope.openFileBrowser = () => - $timeout(() => fileUploadInput.click(), 0, false); - - /** - * A handler that will be invoked when a user selectes a file in the - * file browser. After some error checking, the file will be passed to - * the onFile callback provided to this directive. - * - * @param {Event} e - * The event that was triggered when the user selected a file in - * their file browser. - */ - fileUploadInput.onchange = e => { - - // Process the uploaded file - handleFile(e.target.files[0]); - - // Clear the value to ensure that the change event will be fired - // if the user selects the same file again - fileUploadInput.value = null; - - }; - - }]; - return directive; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css b/guacamole/src/main/frontend/src/app/import/styles/file-upload.css deleted file mode 100644 index afe0fcf97..000000000 --- a/guacamole/src/main/frontend/src/app/import/styles/file-upload.css +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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. - */ - -.file-upload-container { - - display: flex; - flex-direction: column; - align-items: center; - padding: 24px 24px 24px; - - width: fit-content; - - border: 1px solid rgba(0,0,0,.25); - box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); - - margin-left: auto; - margin-right: auto; - -} - -.file-upload-container .upload-header { - - display: flex; - flex-direction: row; - width: 500px; - margin-bottom: 5px; - justify-content: space-between; - -} - -.file-upload-container .file-error { - - color: red; - -} - -.file-upload-container .file-options { - - font-weight: bold; - -} - -.file-upload-container .file-upload-input { - - display: none; - -} - -.file-upload-container .drop-target { - - display: flex; - flex-direction: column; - - align-items: center; - justify-content: space-evenly; - - width: 500px; - height: 200px; - - background: rgba(0,0,0,.04); - border: 1px solid black; - -} - -.file-upload-container .drop-target.file-present { - - background: rgba(0,0,0,.15); - -} - - -.file-upload-container .drop-target .file-name { - - font-weight: bold; - font-size: 1.5em; - -} - -.file-upload-container .drop-target.drop-pending { - - background: #3161a9; - -} - -.file-upload-container .drop-target.drop-pending > * { - - opacity: 0.5; - -} - -.file-upload-container .drop-target .title { - - font-weight: bold; - font-size: 1.25em; - -} - -.file-upload-container .drop-target .browse-link { - - text-decoration: underline; - cursor: pointer; - -} diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 8f038e4d1..3fbdbe077 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -42,3 +42,104 @@ .import .errors .error-message ul { margin: 0px; } + +.file-upload-container { + + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 24px 24px; + + width: fit-content; + + border: 1px solid rgba(0,0,0,.25); + box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); + + margin-left: auto; + margin-right: auto; + +} + +.file-upload-container .upload-header { + + display: flex; + flex-direction: row; + width: 500px; + margin-bottom: 5px; + justify-content: space-between; + +} + +.file-upload-container .file-error { + + color: red; + +} + +.file-upload-container .file-options { + + font-weight: bold; + +} + +.file-upload-container .file-upload-input { + + display: none; + +} + +.file-upload-container .drop-target { + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: space-evenly; + + width: 500px; + height: 200px; + + background: rgba(0,0,0,.04); + border: 1px solid black; + +} + +.file-upload-container .drop-target.file-present { + + background: rgba(0,0,0,.15); + +} + + +.file-upload-container .drop-target .file-name { + + font-weight: bold; + font-size: 1.5em; + +} + +.file-upload-container .drop-target.drop-pending { + + background: #3161a9; + +} + +.file-upload-container .drop-target.drop-pending > * { + + opacity: 0.5; + +} + +.file-upload-container .drop-target .title { + + font-weight: bold; + font-size: 1.25em; + +} + +.file-upload-container .drop-target .browse-link { + + text-decoration: underline; + cursor: pointer; + +} \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index d0f9fd537..c9e37be46 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -4,8 +4,32 @@

      {{'IMPORT.HEADER' | translate}}

      + +
      + +
      + {{'IMPORT.UPLOAD_FILE_TYPES' | translate}} + {{'IMPORT.UPLOAD_HELP_LINK' | translate}} + +
      + +
      + +
      {{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
      + + + + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}} + + +
      {{fileName}}
      + +
      + +
      -
      From eadc1779a0d2b008afd70fa7d9855fec39efe722 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Fri, 3 Mar 2023 19:38:46 +0000 Subject: [PATCH 160/289] GUACAMOLE-926: Import/upload UI improvements. --- .../importConnectionsController.js | 9 ++-- .../frontend/src/app/import/importModule.js | 2 +- .../frontend/src/app/import/styles/import.css | 26 ++++++++++- .../import/templates/connectionImport.html | 44 ++++++++++++------- .../main/frontend/src/translations/en.json | 6 ++- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 14da850f9..124b69555 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -43,7 +43,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const DirectoryPatch = $injector.get('DirectoryPatch'); const ParseError = $injector.get('ParseError'); const PermissionSet = $injector.get('PermissionSet'); - const TranslatableMessage = $injector.get('TranslatableMessage'); const User = $injector.get('User'); const UserGroup = $injector.get('UserGroup'); @@ -424,7 +423,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ /** * Clear the current displayed error. */ - const clearError = () => delete $scope.error; + $scope.clearError = () => delete $scope.error; /** * Process the uploaded import file, importing the connections, granting @@ -503,7 +502,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.cancel = function() { // Clear any error message - clearError(); + $scope.clearError(); // If the upload is in progress, stop it now; the FileReader will // reset the upload state when it stops @@ -544,7 +543,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const handleFile = file => { // Clear any error from a previous attempted file upload - clearError(); + $scope.clearError(); // The MIME type of the provided file const mimeType = file.type; @@ -567,7 +566,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.fileName = file.name; // Clear any error message from the previous upload attempt - clearError(); + $scope.clearError(); // Initialize upload state $scope.aborted = false; diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js index 46e5fa157..164ec2b16 100644 --- a/guacamole/src/main/frontend/src/app/import/importModule.js +++ b/guacamole/src/main/frontend/src/app/import/importModule.js @@ -21,4 +21,4 @@ * The module for code supporting importing user-supplied files. Currently, only * connection import is supported. */ -angular.module('import', ['rest', 'list']); +angular.module('import', ['rest', 'list', 'notification']); diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 3fbdbe077..0b813083d 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -60,6 +60,17 @@ } +.file-upload-container.file-selected { + display: flex; + flex-direction: row; + gap: 100px; +} + +.file-upload-container .clear { + margin: 0; + padding: +} + .file-upload-container .upload-header { display: flex; @@ -142,4 +153,17 @@ text-decoration: underline; cursor: pointer; -} \ No newline at end of file +} + +.import .parseError { + display: flex; + justify-content: center; + padding: 0.75em; + margin-bottom: 10px; + gap: 3em; +} + +.import .parseError .clear { + cursor: pointer; +} + diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index c9e37be46..488b38160 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -4,8 +4,32 @@

      {{'IMPORT.HEADER' | translate}}

      + +
      + + + + + + + {{error.message}} + + + {{'IMPORT.BUTTON_CLEAR_ERROR' | translate}} + +
      + +
      +
      {{fileName}}
      + +
      -
      +
      {{'IMPORT.UPLOAD_FILE_TYPES' | translate}} @@ -17,20 +41,19 @@
      -
      {{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
      +
      {{'IMPORT.UPLOAD_DROP_TITLE' | translate}}
      - + {{'IMPORT.UPLOAD_BROWSE_LINK' | translate}} -
      {{fileName}}
      +
      {{fileName}}
      -
      - -

      - - -

      - {{error.message}} -

      - diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/frontend/src/translations/en.json index c5292b80f..f80e91a5a 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/frontend/src/translations/en.json @@ -187,8 +187,12 @@ "IMPORT": { "BUTTON_CANCEL": "Cancel", + "BUTTON_CLEAR" : "Clear", + "BUTTON_CLEAR_ERROR": "✖", "BUTTON_IMPORT": "Import Connections", + "ERROR_HEADER": "Error", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", "HEADER": "Connection Import", @@ -240,7 +244,7 @@ "TABLE_HEADER_NAME" : "Name", "TABLE_HEADER_PROTOCOL" : "Protocol", "TABLE_HEADER_ERRORS" : "Errors", - "TABLE_HEADER_ROW_NUMBER": "Row Number", + "TABLE_HEADER_ROW_NUMBER": "Row #", "UPLOAD_FILE_TYPES": "CSV, JSON, or YAML", "UPLOAD_HELP_LINK": "View Format Tips", From 7bf192fd73018bb8b61a9626d34d786b648255f0 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 9 Mar 2023 19:14:13 +0000 Subject: [PATCH 161/289] GUACAMOLE-926: Switch to standard notification style. --- .../importConnectionsController.js | 67 ++++++++----------- .../frontend/src/app/import/styles/import.css | 19 ------ .../import/templates/connectionImport.html | 17 ----- .../main/frontend/src/translations/en.json | 3 +- 4 files changed, 30 insertions(+), 76 deletions(-) diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js index 124b69555..ea06979e7 100644 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js @@ -35,6 +35,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const $timeout = $injector.get('$timeout'); const connectionParseService = $injector.get('connectionParseService'); const connectionService = $injector.get('connectionService'); + const guacNotification = $injector.get('guacNotification'); const permissionService = $injector.get('permissionService'); const userService = $injector.get('userService'); const userGroupService = $injector.get('userGroupService'); @@ -45,14 +46,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ const PermissionSet = $injector.get('PermissionSet'); const User = $injector.get('User'); const UserGroup = $injector.get('UserGroup'); - - /** - * Any error that may have occured during import file parsing. - * - * @type {ParseError} - */ - $scope.error = null; - + /** * The result of parsing the current upload, if successful. * @@ -116,7 +110,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ $scope.aborted = false; $scope.dataReady = false; $scope.processing = false; - $scope.error = null; $scope.fileData = null; $scope.mimeType = null; $scope.fileReader = null; @@ -404,7 +397,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } /** - * Set any caught error message to the scope for display. + * Display the provided error to the user in a dismissable dialog. * * @argument {ParseError} error * The error to display. @@ -415,15 +408,24 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // all upload state to allow for a fresh retry resetUploadState(); - // Set the error for display - $scope.error = error; - + guacNotification.showStatus({ + className : 'error', + title : 'IMPORT.DIALOG_HEADER_ERROR', + + // Use the translation key if available + text : { + key: error.key || error.message, + variables: error.variables + }, + + // Add a button to hide the error + actions : [{ + name : 'IMPORT.BUTTON_CLEAR', + callback : () => guacNotification.showStatus(false) + }] + }) + }; - - /** - * Clear the current displayed error. - */ - $scope.clearError = () => delete $scope.error; /** * Process the uploaded import file, importing the connections, granting @@ -455,16 +457,14 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ else if (mimeType.endsWith("yaml")) processDataCallback = connectionParseService.parseYAML; - // We don't expect this to happen - the file upload directive should - // have already have filtered out any invalid file types - else { - handleError(new ParseError({ - message: 'Invalid file type: ' + mimeType, - key: 'IMPORT.INVALID_FILE_TYPE', - variables: { TYPE: mimeType } - })); - return; - } + // The file type was validated before being uploaded - this should + // never happen + else + processDataCallback = () => { + throw new ParseError({ + message: "Unexpected invalid file type: " + mimeType + }); + }; // Make the call to process the data into a series of patches processDataCallback(data) @@ -501,9 +501,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ $scope.cancel = function() { - // Clear any error message - $scope.clearError(); - // If the upload is in progress, stop it now; the FileReader will // reset the upload state when it stops if ($scope.fileReader) { @@ -542,9 +539,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ */ const handleFile = file => { - // Clear any error from a previous attempted file upload - $scope.clearError(); - // The MIME type of the provided file const mimeType = file.type; @@ -556,7 +550,7 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ // If the provided file is not one of the supported types, // display an error and abort processing handleError(new ParseError({ - message: "Invalid file type: " + type, + message: "Invalid file type: " + mimeType, key: 'IMPORT.ERROR_INVALID_FILE_TYPE', variables: { TYPE: mimeType } })); @@ -564,9 +558,6 @@ angular.module('import').controller('importConnectionsController', ['$scope', '$ } $scope.fileName = file.name; - - // Clear any error message from the previous upload attempt - $scope.clearError(); // Initialize upload state $scope.aborted = false; diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/frontend/src/app/import/styles/import.css index 0b813083d..656571c63 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/frontend/src/app/import/styles/import.css @@ -17,10 +17,6 @@ * under the License. */ -.import .parseError { - color: red; -} - .import .import-buttons { margin-top: 10px; @@ -30,7 +26,6 @@ } - .import .errors table { width: 100%; } @@ -68,7 +63,6 @@ .file-upload-container .clear { margin: 0; - padding: } .file-upload-container .upload-header { @@ -154,16 +148,3 @@ cursor: pointer; } - -.import .parseError { - display: flex; - justify-content: center; - padding: 0.75em; - margin-bottom: 10px; - gap: 3em; -} - -.import .parseError .clear { - cursor: pointer; -} - diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html index 488b38160..e33959e66 100644 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html @@ -5,23 +5,6 @@
      -
      - - - - - - - {{error.message}} - - - {{'IMPORT.BUTTON_CLEAR_ERROR' | translate}} - -
      -
      {{fileName}}