GUACAMOLE-641: Merge support for reading secrets from key vaults.

This commit is contained in:
James Muehlner
2022-01-25 20:47:14 -08:00
committed by GitHub
48 changed files with 3436 additions and 25 deletions

View File

@@ -19,6 +19,7 @@
package org.apache.guacamole.net.auth;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
@@ -28,7 +29,9 @@ import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* Connection implementation which overrides the connect() function of an
* underlying Connection, adding a given set of parameter tokens to the tokens
* already supplied.
* already supplied. If not supplying a static set of tokens at construction
* time, implementations should override either {@link #addTokens(java.util.Map)}
* or {@link #getTokens()} to provide tokens dynamically.
*/
public class TokenInjectingConnection extends DelegatingConnection {
@@ -37,6 +40,39 @@ public class TokenInjectingConnection extends DelegatingConnection {
*/
private final Map<String, String> tokens;
/**
* Returns the tokens which should be added to an in-progress call to
* connect(). If not overridden, this function will return the tokens
* provided when this instance of TokenInjectingConnection was created. If
* the values of existing tokens need to be considered, implementations
* should override {@link #addTokens(java.util.Map)} instead.
*
* @return
* The tokens which should be added to the in-progress call to
* connect().
*
* @throws GuacamoleException
* If the applicable tokens cannot be generated.
*/
protected Map<String, String> getTokens() throws GuacamoleException {
return tokens;
}
/**
* Adds tokens to an in-progress call to connect(). If not overridden, this
* function will add the tokens returned by {@link #getTokens()}.
*
* @param tokens
* A modifiable Map containing the tokens already supplied to
* connect().
*
* @throws GuacamoleException
* If the applicable tokens cannot be generated.
*/
protected void addTokens(Map<String, String> tokens) throws GuacamoleException {
tokens.putAll(getTokens());
}
/**
* Wraps the given Connection, automatically adding the given tokens to
* each invocation of connect(). Any additional tokens which have the same
@@ -54,13 +90,27 @@ public class TokenInjectingConnection extends DelegatingConnection {
this.tokens = tokens;
}
/**
* Wraps the given Connection such that the additional parameter tokens
* added by {@link #addTokens(java.util.Map)} or returned by
* {@link #getTokens()} are included with each invocation of connect().
* Any additional tokens which have the same name as existing tokens will
* override the existing values.
*
* @param connection
* The Connection to wrap.
*/
public TokenInjectingConnection(Connection connection) {
this(connection, Collections.<String, String>emptyMap());
}
@Override
public GuacamoleTunnel connect(GuacamoleClientInformation info,
Map<String, String> tokens) throws GuacamoleException {
// Apply provided tokens over those given to connect()
tokens = new HashMap<>(tokens);
tokens.putAll(this.tokens);
addTokens(tokens);
return super.connect(info, tokens);

View File

@@ -19,6 +19,7 @@
package org.apache.guacamole.net.auth;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
@@ -28,7 +29,10 @@ import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* ConnectionGroup implementation which overrides the connect() function of an
* underlying ConnectionGroup, adding a given set of parameter tokens to the
* tokens already supplied.
* tokens already supplied. If not supplying a static set of tokens at
* construction time, implementations should override either
* {@link #addTokens(java.util.Map)} or {@link #getTokens()} to provide tokens
* dynamically.
*/
public class TokenInjectingConnectionGroup extends DelegatingConnectionGroup {
@@ -37,6 +41,39 @@ public class TokenInjectingConnectionGroup extends DelegatingConnectionGroup {
*/
private final Map<String, String> tokens;
/**
* Returns the tokens which should be added to an in-progress call to
* connect(). If not overridden, this function will return the tokens
* provided when this instance of TokenInjectingConnection was created. If
* the values of existing tokens need to be considered, implementations
* should override {@link #addTokens(java.util.Map)} instead.
*
* @return
* The tokens which should be added to the in-progress call to
* connect().
*
* @throws GuacamoleException
* If the applicable tokens cannot be generated.
*/
protected Map<String, String> getTokens() throws GuacamoleException {
return tokens;
}
/**
* Adds tokens to an in-progress call to connect(). If not overridden, this
* function will add the tokens returned by {@link #getTokens()}.
*
* @param tokens
* A modifiable Map containing the tokens already supplied to
* connect().
*
* @throws GuacamoleException
* If the applicable tokens cannot be generated.
*/
protected void addTokens(Map<String, String> tokens) throws GuacamoleException {
tokens.putAll(getTokens());
}
/**
* Wraps the given ConnectionGroup, automatically adding the given tokens
* to each invocation of connect(). Any additional tokens which have the
@@ -54,13 +91,27 @@ public class TokenInjectingConnectionGroup extends DelegatingConnectionGroup {
this.tokens = tokens;
}
/**
* Wraps the given ConnectionGroup such that the additional parameter
* tokens added by {@link #addTokens(java.util.Map)} or returned by
* {@link #getTokens()} are included with each invocation of connect(). Any
* additional tokens which have the same name as existing tokens will
* override the existing values.
*
* @param connectionGroup
* The ConnectionGroup to wrap.
*/
public TokenInjectingConnectionGroup(ConnectionGroup connectionGroup) {
this(connectionGroup, Collections.<String, String>emptyMap());
}
@Override
public GuacamoleTunnel connect(GuacamoleClientInformation info,
Map<String, String> tokens) throws GuacamoleException {
// Apply provided tokens over those given to connect()
tokens = new HashMap<>(tokens);
tokens.putAll(this.tokens);
addTokens(tokens);
return super.connect(info, tokens);

View File

@@ -32,7 +32,7 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
/**
* The additional tokens to include with each call to connect() if
* getTokens() is not overridden.
* getTokens() or addTokens() are not overridden.
*/
private final Map<String, String> tokens;
@@ -42,8 +42,8 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
* parameter tokens are included. Any additional tokens which have the same
* name as existing tokens will override the existing values. If tokens
* specific to a particular connection or connection group need to be
* included, getTokens() may be overridden to provide a different set of
* tokens.
* included, getTokens() or addTokens() may be overridden to provide a
* different set of tokens.
*
* @param userContext
* The UserContext to wrap.
@@ -60,9 +60,9 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
/**
* Wraps the given UserContext, overriding the connect() function of each
* retrieved Connection and ConnectionGroup such that the additional
* parameter tokens returned by getTokens() are included. Any additional
* tokens which have the same name as existing tokens will override the
* existing values.
* parameter tokens added by addTokens() or returned by getTokens() are
* included. Any additional tokens which have the same name as existing
* tokens will override the existing values.
*
* @param userContext
* The UserContext to wrap.
@@ -75,7 +75,10 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
* Returns the tokens which should be added to an in-progress call to
* connect() for the given Connection. If not overridden, this function
* will return the tokens provided when this instance of
* TokenInjectingUserContext was created.
* TokenInjectingUserContext was created. If the values of existing tokens
* need to be considered, implementations should override
* {@link #addTokens(org.apache.guacamole.net.auth.Connection, java.util.Map)}
* instead.
*
* @param connection
* The Connection on which connect() has been called.
@@ -83,16 +86,45 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
* @return
* The tokens which should be added to the in-progress call to
* connect().
*
* @throws GuacamoleException
* If the tokens applicable to the given connection cannot be
* generated.
*/
protected Map<String, String> getTokens(Connection connection) {
protected Map<String, String> getTokens(Connection connection)
throws GuacamoleException {
return tokens;
}
/**
* Adds tokens to an in-progress call to connect() for the given
* Connection. If not overridden, this function will add the tokens
* returned by {@link #getTokens(org.apache.guacamole.net.auth.Connection)}.
*
* @param connection
* The Connection on which connect() has been called.
*
* @param tokens
* A modifiable Map containing the tokens already supplied to
* connect().
*
* @throws GuacamoleException
* If the tokens applicable to the given connection cannot be
* generated.
*/
protected void addTokens(Connection connection, Map<String, String> tokens)
throws GuacamoleException {
tokens.putAll(getTokens(connection));
}
/**
* Returns the tokens which should be added to an in-progress call to
* connect() for the given ConnectionGroup. If not overridden, this
* function will return the tokens provided when this instance of
* TokenInjectingUserContext was created.
* TokenInjectingUserContext was created. If the values of existing tokens
* need to be considered, implementations should override
* {@link #addTokens(org.apache.guacamole.net.auth.ConnectionGroup, java.util.Map)}
* instead.
*
* @param connectionGroup
* The ConnectionGroup on which connect() has been called.
@@ -100,11 +132,37 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
* @return
* The tokens which should be added to the in-progress call to
* connect().
*
* @throws GuacamoleException
* If the tokens applicable to the given connection group cannot be
* generated.
*/
protected Map<String, String> getTokens(ConnectionGroup connectionGroup) {
protected Map<String, String> getTokens(ConnectionGroup connectionGroup)
throws GuacamoleException {
return tokens;
}
/**
* Adds tokens to an in-progress call to connect() for the given
* ConnectionGroup. If not overridden, this function will add the tokens
* returned by {@link #getTokens(org.apache.guacamole.net.auth.ConnectionGroup)}.
*
* @param connectionGroup
* The ConnectionGroup on which connect() has been called.
*
* @param tokens
* A modifiable Map containing the tokens already supplied to
* connect().
*
* @throws GuacamoleException
* If the tokens applicable to the given connection cannot be
* generated.
*/
protected void addTokens(ConnectionGroup connectionGroup,
Map<String, String> tokens) throws GuacamoleException {
tokens.putAll(getTokens(connectionGroup));
}
@Override
public Directory<ConnectionGroup> getConnectionGroupDirectory()
throws GuacamoleException {
@@ -112,7 +170,14 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
@Override
protected ConnectionGroup decorate(ConnectionGroup object) throws GuacamoleException {
return new TokenInjectingConnectionGroup(object, getTokens(object));
return new TokenInjectingConnectionGroup(object) {
@Override
protected void addTokens(Map<String, String> tokens) throws GuacamoleException {
TokenInjectingUserContext.this.addTokens(object, tokens);
}
};
}
@Override
@@ -130,7 +195,14 @@ public class TokenInjectingUserContext extends DelegatingUserContext {
@Override
protected Connection decorate(Connection object) throws GuacamoleException {
return new TokenInjectingConnection(object, getTokens(object));
return new TokenInjectingConnection(object) {
@Override
protected void addTokens(Map<String, String> tokens) throws GuacamoleException {
TokenInjectingUserContext.this.addTokens(object, tokens);
}
};
}
@Override

View File

@@ -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.token;
import org.apache.guacamole.GuacamoleServerException;
/**
* An exception thrown when a token cannot be substituted because it has no
* corresponding value. Additional information describing the undefined token
* is provided.
*/
public class GuacamoleTokenUndefinedException extends GuacamoleServerException {
/**
* The name of the token that is undefined.
*/
private final String tokenName;
/**
* Creates a new GuacamoleTokenUndefinedException with the given message,
* cause, and associated undefined token name.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param cause
* The cause of this exception.
*
* @param tokenName
* The name of the token which has no defined value.
*/
public GuacamoleTokenUndefinedException(String message, Throwable cause,
String tokenName) {
super(message, cause);
this.tokenName = tokenName;
}
/**
* Creates a new GuacamoleTokenUndefinedException with the given
* message and associated undefined token name.
*
* @param message
* A human readable description of the exception that occurred.
*
* @param tokenName
* The name of the token which has no defined value.
*/
public GuacamoleTokenUndefinedException(String message, String tokenName) {
super(message);
this.tokenName = tokenName;
}
/**
* Creates a new GuacamoleTokenUndefinedException with the given cause
* and associated undefined token name.
*
* @param cause
* The cause of this exception.
*
* @param tokenName
* The name of the token which has no defined value.
*/
public GuacamoleTokenUndefinedException(Throwable cause, String tokenName) {
super(cause);
this.tokenName = tokenName;
}
/**
* Returns the name of the token which has no defined value, causing this
* exception to be thrown.
*
* @return
* The name of the token which has no defined value.
*/
public String getTokenName() {
return tokenName;
}
}

View File

@@ -162,19 +162,31 @@ public class TokenFilter {
tokenValues.clear();
tokenValues.putAll(tokens);
}
/**
* Filters the given string, replacing any tokens with their corresponding
* values.
* values. Handling of undefined tokens depends on the value given for the
* strict flag.
*
* @param input
* The string to filter.
*
* @param strict
* Whether to disallow tokens which lack values from existing in the
* string. If true, an exception will be thrown if any tokens in the
* string lack corresponding values. If false, tokens which lack values
* will be interpreted as literals.
*
* @return
* A copy of the input string, with any tokens replaced with their
* corresponding values.
*
* @throws GuacamoleTokenUndefinedException
* If the strict flag is set to true and at least one token in the
* given string has no corresponding value.
*/
public String filter(String input) {
private String filter(String input, boolean strict)
throws GuacamoleTokenUndefinedException {
StringBuilder output = new StringBuilder();
Matcher tokenMatcher = tokenPattern.matcher(input);
@@ -209,10 +221,20 @@ public class TokenFilter {
String tokenName = tokenMatcher.group(TOKEN_NAME_GROUP);
String tokenValue = getToken(tokenName);
// If token is unknown, interpret as literal
// If token is unknown, interpretation depends on whether
// strict mode is enabled
if (tokenValue == null) {
// Fail outright if strict mode is enabled
if (strict)
throw new GuacamoleTokenUndefinedException("Token "
+ "has no defined value.", tokenName);
// If strict mode is NOT enabled, simply interpret as
// a literal
String notToken = tokenMatcher.group(TOKEN_GROUP);
output.append(notToken);
}
// Otherwise, check for modifiers and substitute value appropriately
@@ -247,19 +269,71 @@ public class TokenFilter {
// Update last regex match
endOfLastMatch = tokenMatcher.end();
}
// Append any remaining non-token text
output.append(input.substring(endOfLastMatch));
return output.toString();
}
/**
* Filters the given string, replacing any tokens with their corresponding
* values. Any tokens present in the given string which lack values will
* be interpreted as literals.
*
* @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) {
// Filter with strict mode disabled (should always succeed)
try {
return filter(input, false);
}
// GuacamoleTokenUndefinedException cannot be thrown when strict mode
// is disabled
catch (GuacamoleTokenUndefinedException e) {
throw new IllegalStateException("filter() threw "
+ "GuacamoleTokenUndefinedException despite strict mode "
+ "being disabled.");
}
}
/**
* Filters the given string, replacing any tokens with their corresponding
* values. If any token in the given string has no defined value within
* this TokenFilter, a GuacamoleTokenUndefinedException will be thrown.
*
* @param input
* The string to filter.
*
* @return
* A copy of the input string, with any tokens replaced with their
* corresponding values.
*
* @throws GuacamoleTokenUndefinedException
* If at least one token in the given string has no corresponding
* value.
*/
public String filterStrict(String input)
throws GuacamoleTokenUndefinedException {
return filter(input, true);
}
/**
* Given an arbitrary map containing String values, replace each non-null
* value with the corresponding filtered value.
* value with the corresponding filtered value. Any tokens present in the
* values of the given map which lack defined values within this
* TokenFilter will be interpreted as literals.
*
* @param map
* The map whose values should be filtered.
@@ -273,6 +347,34 @@ public class TokenFilter {
String value = entry.getValue();
if (value != null)
entry.setValue(filter(value));
}
}
/**
* Given an arbitrary map containing String values, replace each non-null
* value with the corresponding filtered value. If any token in any string
* has no defined value within this TokenFilter, a
* GuacamoleTokenUndefinedException will be thrown.
*
* @param map
* The map whose values should be filtered.
*
* @throws GuacamoleTokenUndefinedException
* If at least one token in at least one string has no corresponding
* value.
*/
public void filterValuesStrict(Map<?, String> map)
throws GuacamoleTokenUndefinedException {
// For each map entry
for (Map.Entry<?, String> entry : map.entrySet()) {
// If value is non-null, filter value through this TokenFilter
String value = entry.getValue();
if (value != null)
entry.setValue(filterStrict(value));
}