mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-1661: Match by both user and domain when using KEEPER_USER_ tokens.
This commit is contained in:
@@ -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;
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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<String, KeeperRecord> cachedRecordsByUsername = new HashMap<>();
|
||||
private final Map<UserDomain, KeeperRecord> 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<String> cachedAmbiguousUsernames = new HashSet<>();
|
||||
private final Set<UserDomain> 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()) {
|
||||
|
||||
// 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());
|
||||
}
|
||||
if (usernameAndDomain.hasDomain())
|
||||
domain = usernameAndDomain.getDomain();
|
||||
addRecordForDomain(record, domain);
|
||||
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user