diff --git a/guacamole-ext/pom.xml b/guacamole-ext/pom.xml index e175b300d..ba0b84813 100644 --- a/guacamole-ext/pom.xml +++ b/guacamole-ext/pom.xml @@ -112,6 +112,14 @@ compile + + + junit + junit + 4.10 + test + + diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java index ab9d98556..f899ce036 100644 --- a/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/net/auth/simple/SimpleAuthenticationProvider.java @@ -28,6 +28,8 @@ import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; +import org.glyptodon.guacamole.token.StandardTokens; +import org.glyptodon.guacamole.token.TokenFilter; /** * Provides means of retrieving a set of named GuacamoleConfigurations for a @@ -72,6 +74,14 @@ public abstract class SimpleAuthenticationProvider if (configs == null) return null; + // Build credential TokenFilter + TokenFilter tokenFilter = new TokenFilter(); + StandardTokens.addStandardTokens(tokenFilter, credentials); + + // Filter each configuration + for (GuacamoleConfiguration config : configs.values()) + tokenFilter.filterValues(config.getParameters()); + // Return user context restricted to authorized configs return new SimpleUserContext(credentials.getUsername(), configs); diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/StandardTokens.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/StandardTokens.java new file mode 100644 index 000000000..31b2c4eea --- /dev/null +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/StandardTokens.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * 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. + */ + +package org.glyptodon.guacamole.token; + +import org.glyptodon.guacamole.net.auth.Credentials; + +/** + * Utility class which provides access to standardized token names, as well as + * facilities for generating those tokens from common objects. + * + * @author Michael Jumper + */ +public class StandardTokens { + + /** + * The name of the username token added via addStandardTokens(). + */ + private static final String USERNAME_TOKEN = "GUAC_USERNAME"; + + /** + * The name of the password token added via addStandardTokens(). + */ + private static final String PASSWORD_TOKEN = "GUAC_PASSWORD"; + + /** + * This utility class should not be instantiated. + */ + private StandardTokens() {} + + /** + * Adds the standard username (GUAC_USERNAME) and password (GUAC_PASSWORD) + * tokens to the given TokenFilter using the values from the given + * Credentials object. If either the username or password are not set + * within the given credentials, the corresponding token(s) will remain + * unset. + * + * @param filter + * The TokenFilter to add standard username/password tokens to. + * + * @param credentials + * The Credentials containing the username/password to add. + * + */ + public static void addStandardTokens(TokenFilter filter, Credentials credentials) { + + // Add username token + String username = credentials.getUsername(); + if (username != null) + filter.setToken(USERNAME_TOKEN, username); + + // Add password token + String password = credentials.getPassword(); + if (password != null) + filter.setToken(PASSWORD_TOKEN, password); + + } + + +} diff --git a/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/TokenFilter.java b/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/TokenFilter.java new file mode 100644 index 000000000..56a5dbabc --- /dev/null +++ b/guacamole-ext/src/main/java/org/glyptodon/guacamole/token/TokenFilter.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * 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. + */ + +package org.glyptodon.guacamole.token; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Filtering object which replaces tokens of the form "${TOKEN_NAME}" with + * their corresponding values. Unknown tokens are not replaced. If TOKEN_NAME + * is a valid token, the literal value "${TOKEN_NAME}" can be included by using + * "$${TOKEN_NAME}". + * + * @author Michael Jumper + */ +public class TokenFilter { + + /** + * Regular expression which matches individual tokens, with additional + * capturing groups for convenient retrieval of leading text, the possible + * escape character preceding the token, the name of the token, and the + * entire token itself. + */ + private final Pattern tokenPattern = Pattern.compile("(.*?)(^|.)(\\$\\{([A-Za-z0-9_]*)\\})"); + + /** + * The index of the capturing group within tokenPattern which matches + * non-token text preceding a possible token. + */ + private static final int LEADING_TEXT_GROUP = 1; + + /** + * The index of the capturing group within tokenPattern which matches the + * character immediately preceding a possible token, possibly denoting that + * the token should instead be interpreted as a literal. + */ + private static final int ESCAPE_CHAR_GROUP = 2; + + /** + * The index of the capturing group within tokenPattern which matches the + * entire token, including the leading "${" and terminating "}" strings. + */ + private static final int TOKEN_GROUP = 3; + + /** + * The index of the capturing group within tokenPattern which matches only + * the token name contained within the "${" and "}" strings. + */ + private static final int TOKEN_NAME_GROUP = 4; + + /** + * The values of all known tokens. + */ + private final Map tokenValues = new HashMap(); + + /** + * Sets the token having the given name to the given value. Any existing + * value for that token is replaced. + * + * @param name + * The name of the token to set. + * + * @param value + * The value to set the token to. + */ + public void setToken(String name, String value) { + tokenValues.put(name, value); + } + + /** + * Returns the value of the token with the given name, or null if no such + * token has been set. + * + * @param name + * The name of the token to return. + * + * @return + * The value of the token with the given name, or null if no such + * token exists. + */ + public String getToken(String name) { + return tokenValues.get(name); + } + + /** + * Removes the value of the token with the given name. If no such token + * exists, this function has no effect. + * + * @param name + * The name of the token whose value should be removed. + */ + public void unsetToken(String name) { + tokenValues.remove(name); + } + + /** + * Returns a map of all tokens, with each key being a token name, and each + * value being the corresponding token value. Changes to this map will + * directly affect the tokens associated with this filter. + * + * @return + * A map of all token names and their corresponding values. + */ + public Map getTokens() { + return tokenValues; + } + + /** + * Replaces all current token values with the contents of the given map, + * where each map key represents a token name, and each map value + * represents a token value. + * + * @param tokens + * A map containing the token names and corresponding values to + * assign. + */ + public void setTokens(Map tokens) { + tokenValues.clear(); + tokenValues.putAll(tokens); + } + + /** + * Filters the given string, replacing any tokens with their corresponding + * values. + * + * @param input + * The string to filter. + * + * @return + * A copy of the input string, with any tokens replaced with their + * corresponding values. + */ + public String filter(String input) { + + StringBuilder output = new StringBuilder(); + Matcher tokenMatcher = tokenPattern.matcher(input); + + // Track last regex match + int endOfLastMatch = 0; + + // For each possible token + while (tokenMatcher.find()) { + + // Pull possible leading text and first char before possible token + String literal = tokenMatcher.group(LEADING_TEXT_GROUP); + String escape = tokenMatcher.group(ESCAPE_CHAR_GROUP); + + // Append leading non-token text + output.append(literal); + + // If char before token is '$', the token itself is escaped + if ("$".equals(escape)) { + String notToken = tokenMatcher.group(TOKEN_GROUP); + output.append(notToken); + } + + // If char is not '$', interpret as a token + else { + + // The char before the token, if any, is a literal + output.append(escape); + + // Pull token value + String tokenName = tokenMatcher.group(TOKEN_NAME_GROUP); + String tokenValue = getToken(tokenName); + + // If token is unknown, interpret as literal + if (tokenValue == null) { + String notToken = tokenMatcher.group(TOKEN_GROUP); + output.append(notToken); + } + + // Otherwise, substitute value + else + output.append(tokenValue); + + } + + // Update last regex match + endOfLastMatch = tokenMatcher.end(); + + } + + // Append any remaining non-token text + output.append(input.substring(endOfLastMatch)); + + return output.toString(); + + } + + /** + * Given an arbitrary map containing String values, replace each non-null + * value with the corresponding filtered value. + * + * @param map + * The map whose values should be filtered. + */ + public void filterValues(Map map) { + + // For each map entry + for (Map.Entry entry : map.entrySet()) { + + // If value is non-null, filter value through this TokenFilter + String value = entry.getValue(); + if (value != null) + entry.setValue(filter(value)); + + } + + } + +} diff --git a/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.java b/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.java new file mode 100644 index 000000000..9d0777c31 --- /dev/null +++ b/guacamole-ext/src/test/java/org/glyptodon/guacamole/token/TokenFilterTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * 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. + */ + +package org.glyptodon.guacamole.token; + +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Test which verifies the filtering functionality of TokenFilter. + * + * @author Michael Jumper + */ +public class TokenFilterTest { + + /** + * Verifies that token replacement via filter() functions as specified. + */ + @Test + public void testFilter() { + + // Create token filter + TokenFilter tokenFilter = new TokenFilter(); + tokenFilter.setToken("TOKEN_A", "value-of-a"); + tokenFilter.setToken("TOKEN_B", "value-of-b"); + + // Test basic substitution and escaping + assertEquals( + "$${NOPE}hellovalue-of-aworldvalue-of-b${NOT_A_TOKEN}", + tokenFilter.filter("$$${NOPE}hello${TOKEN_A}world${TOKEN_B}$${NOT_A_TOKEN}") + ); + + // Unknown tokens must be interpreted as literals + assertEquals( + "${NOPE}hellovalue-of-aworld${TOKEN_C}", + tokenFilter.filter("${NOPE}hello${TOKEN_A}world${TOKEN_C}") + ); + + } + + /** + * Verifies that token replacement via filterValues() functions as + * specified. + */ + @Test + public void testFilterValues() { + + // Create token filter + TokenFilter tokenFilter = new TokenFilter(); + tokenFilter.setToken("TOKEN_A", "value-of-a"); + tokenFilter.setToken("TOKEN_B", "value-of-b"); + + // Create test map + Map map = new HashMap(); + map.put(1, "$$${NOPE}hello${TOKEN_A}world${TOKEN_B}$${NOT_A_TOKEN}"); + map.put(2, "${NOPE}hello${TOKEN_A}world${TOKEN_C}"); + map.put(3, null); + + // Filter map values + tokenFilter.filterValues(map); + + // Filter should not affect size of map + assertEquals(3, map.size()); + + // Filtered value 1 + assertEquals( + "$${NOPE}hellovalue-of-aworldvalue-of-b${NOT_A_TOKEN}", + map.get(1) + ); + + // Filtered value 2 + assertEquals( + "${NOPE}hellovalue-of-aworld${TOKEN_C}", + map.get(2) + ); + + // Null values are not filtered + assertNull(map.get(3)); + + } + +} diff --git a/guacamole/src/main/webapp/app/manage/directives/connectionParameter.js b/guacamole/src/main/webapp/app/manage/directives/connectionParameter.js index 49d74d6c4..8ca090163 100644 --- a/guacamole/src/main/webapp/app/manage/directives/connectionParameter.js +++ b/guacamole/src/main/webapp/app/manage/directives/connectionParameter.js @@ -63,6 +63,51 @@ angular.module('manage').directive('guacConnectionParameter', [function connecti var $q = $injector.get('$q'); var translationStringService = $injector.get('translationStringService'); + /** + * The type to use for password input fields. By default, password + * input fields have type 'password', and are thus masked. + * + * @type String + * @default 'password' + */ + $scope.passwordInputType = 'password'; + + /** + * Returns a string which describes the action the next call to + * togglePassword() will have. + * + * @return {String} + * A string which describes the action the next call to + * togglePassword() will have. + */ + $scope.getTogglePasswordHelpText = function getTogglePasswordHelpText() { + + // If password is hidden, togglePassword() will show the password + if ($scope.passwordInputType === 'password') + return 'MANAGE.HELP_SHOW_PASSWORD'; + + // If password is shown, togglePassword() will hide the password + return 'MANAGE.HELP_HIDE_PASSWORD'; + + }; + + /** + * Toggles visibility of the parameter contents, if this parameter + * is a password parameter. Initially, password contents are + * masked (invisible). + */ + $scope.togglePassword = function togglePassword() { + + // If password is hidden, show the password + if ($scope.passwordInputType === 'password') + $scope.passwordInputType = 'text'; + + // If password is shown, hide the password + else + $scope.passwordInputType = 'password'; + + }; + /** * Deferred load of the parameter definition, pending availability * of the protocol definition as a whole. diff --git a/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css b/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css new file mode 100644 index 000000000..11c78f105 --- /dev/null +++ b/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * 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. + */ + +/* Do not stretch connection parameters to fit available area */ +.connection-parameter input[type=text], +.connection-parameter input[type=password], +.connection-parameter input[type=number] { + width: auto; +} + +/* Keep toggle-password icon on same line */ +.connection-parameter .password-field { + white-space: nowrap; +} + +/* Generic 1x1em icon/button */ +.connection-parameter .password-field .icon.toggle-password { + + display: inline-block; + opacity: 0.5; + cursor: default; + + background-repeat: no-repeat; + background-size: 1em; + width: 1em; + height: 1em; + +} + +/* Icon for unmasking passwords */ +.connection-parameter .password-field input[type=password] ~ .icon.toggle-password { + background-image: url('images/action-icons/guac-show-pass.png'); +} + +/* Icon for masking passwords */ +.connection-parameter .password-field input[type=text] ~ .icon.toggle-password { + background-image: url('images/action-icons/guac-hide-pass.png'); +} \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/manage/templates/connectionParameter.html b/guacamole/src/main/webapp/app/manage/templates/connectionParameter.html index 8a5ec79b7..7cbd07c10 100644 --- a/guacamole/src/main/webapp/app/manage/templates/connectionParameter.html +++ b/guacamole/src/main/webapp/app/manage/templates/connectionParameter.html @@ -1,4 +1,4 @@ - +
+
+ +
+
+ - \ No newline at end of file +
\ No newline at end of file diff --git a/guacamole/src/main/webapp/images/action-icons/guac-hide-pass.png b/guacamole/src/main/webapp/images/action-icons/guac-hide-pass.png new file mode 100644 index 000000000..816d0b26d Binary files /dev/null and b/guacamole/src/main/webapp/images/action-icons/guac-hide-pass.png differ diff --git a/guacamole/src/main/webapp/images/action-icons/guac-show-pass.png b/guacamole/src/main/webapp/images/action-icons/guac-show-pass.png new file mode 100644 index 000000000..ce7ee2b2a Binary files /dev/null and b/guacamole/src/main/webapp/images/action-icons/guac-show-pass.png differ diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index 9055ecb36..ec73b5a31 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -135,8 +135,10 @@ "DIALOG_HEADER_ERROR" : "Error", - "HELP_CONNECTIONS" : "Click or tap on a connection below to manage that connection. Depending on your access level, connections can be added and deleted, and their properties (protocol, hostname, port, etc.) can be changed.", - "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_CONNECTIONS" : "Click or tap on a connection below to manage that connection. Depending on your access level, connections can be added and deleted, and their properties (protocol, hostname, port, etc.) can be changed.", + "HELP_SHOW_PASSWORD" : "Click to show password", + "HELP_HIDE_PASSWORD" : "Click to hide password", + "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.", "SECTION_HEADER_ADMINISTRATION" : "Administration", "SECTION_HEADER_CONNECTIONS" : "Connections",