Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GUACAMOLE-1881: Add parameter token containing the domain of LDAP/AD usernames. #987

Merged
merged 4 commits into from
Aug 18, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
Expand Down Expand Up @@ -63,7 +65,36 @@ public class AuthenticationProviderService {
/**
* The prefix that will be used when generating tokens.
*/
public static final String LDAP_ATTRIBUTE_TOKEN_PREFIX = "LDAP_";
public static final String LDAP_TOKEN_PREFIX = "LDAP_";

/**
* Regular expression that extracts the Windows / Active Directory domain
* from a username in either of the following formats: down-level logon
* ("DOMAIN\\username") or UPN ("username@domain"). If the username is in
* "DOMAIN\\username" format, the domain will be stored in the first
necouchman marked this conversation as resolved.
Show resolved Hide resolved
* capturing group. If the username is in UPN format, the domain will be
* stored in the second capturing group.
*/
private static final Pattern LDAP_USERNAME_DOMAIN = Pattern.compile("^(.+)\\\\.*$|^.*@(.+)$");

/**
* The index of the capturing group in {@link #LDAP_USERNAME_DOMAIN} that
* captures the domain of a username in down-level logon format
* ("DOMAIN\\username").
*/
private static final int LDAP_USERNAME_DOMAIN_DOWN_LEVEL_GROUP = 1;

/**
* The index of the capturing group in {@link #LDAP_USERNAME_DOMAIN} that
* captures the domain of a username in UPN format ("username@domain").
*/
private static final int LDAP_USERNAME_DOMAIN_UPN_GROUP = 2;

/**
* The name of parameter token that will contain the domain extracted from
* the LDAP user's username, if applicable.
*/
public static final String LDAP_DOMAIN_TOKEN = LDAP_TOKEN_PREFIX + "DOMAIN";

/**
* Service for creating and managing connections to LDAP servers.
Expand Down Expand Up @@ -286,18 +317,18 @@ public LDAPAuthenticatedUser authenticateUser(Credentials credentials)
CredentialsInfo.USERNAME_PASSWORD);

try {

// Retrieve group membership of the user that just authenticated
Set<String> effectiveGroups =
userGroupService.getParentUserGroupIdentifiers(config, config.getBindDN());

// Return AuthenticatedUser if bind succeeds
LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();

authenticatedUser.init(config, credentials,
getAttributeTokens(config), effectiveGroups);
getUserTokens(config, credentials), effectiveGroups);

return authenticatedUser;

}

catch (GuacamoleException | RuntimeException | Error e) {
Expand All @@ -308,25 +339,65 @@ public LDAPAuthenticatedUser authenticateUser(Credentials credentials)
}

/**
* Returns parameter tokens generated from LDAP attributes on the user
* currently bound under the given LDAP connection. The attributes to be
* converted into parameter tokens must be explicitly listed in
* guacamole.properties. If no attributes are specified or none are
* found on the LDAP user object, an empty map is returned.
* Returns the Windows / Active Directory domain included in the username
* of the provided user credentials. If the username does not contain a
* domain, null is returned.
*
* @param credentials
* The credentials used for authentication.
*
* @return
* The domain name within the username of the provided credentials, or
* null if no domain is present.
*/
private String getUserDomain(Credentials credentials) {

// Extract domain from username (not necessarily present)
String username = credentials.getUsername();
Matcher matcher = LDAP_USERNAME_DOMAIN.matcher(username);
if (!matcher.find())
return null;

String dlDomain = matcher.group(LDAP_USERNAME_DOMAIN_DOWN_LEVEL_GROUP);
String upnDomain = matcher.group(LDAP_USERNAME_DOMAIN_UPN_GROUP);

// The two domain formats are mutually exclusive. If any format is
// present, the other must be absent unless there is a bug in the
// way the domains are parsed
assert(dlDomain == null || upnDomain == null);

// Use whichever domain format is present
return dlDomain != null ? dlDomain : upnDomain;

}

/**
* Returns parameter tokens generated based on details specific to the user
* currently bound under the given LDAP connection. Both LDAP attributes on
* the user's associated LDAP object and the credentials submitted by the user
* to Guacamole are considered. If any tokens are to be derived from LDAP
* attributes, those attributes must be explicitly listed in
* guacamole.properties. If no tokens are applicable, an empty map is returned.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* A map of parameter tokens generated from attributes on the user
* currently bound under the given LDAP connection, as a map of token
* name to corresponding value, or an empty map if no attributes are
* specified or none are found on the user object.
* A map of parameter tokens. These tokens are generated based on
* the attributes of the user currently bound under the given LDAP connection
* and the user's credentials. The map's keys are the canonicalized attribute
* names with an "LDAP_" prefix, and the values are the corresponding attribute
* values. If the domain name is extracted from the user's credentials, it is added
* to the map with the key "LDAP_DOMAIN". If no applicable tokens are found,
* the method returns an empty map.
*
* @throws GuacamoleException
* If an error occurs retrieving the user DN or the attributes.
*/
private Map<String, String> getAttributeTokens(ConnectedLDAPConfiguration config)
private Map<String, String> getUserTokens(ConnectedLDAPConfiguration config, Credentials credentials)
throws GuacamoleException {

// Get attributes from configuration information
Expand Down Expand Up @@ -354,14 +425,19 @@ private Map<String, String> getAttributeTokens(ConnectedLDAPConfiguration config
// Convert each retrieved attribute into a corresponding token
for (Attribute attr : attributes) {
tokens.put(TokenName.canonicalize(attr.getId(),
LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getString());
LDAP_TOKEN_PREFIX), attr.getString());
}

}
catch (LdapException e) {
throw new GuacamoleServerException("Could not query LDAP user attributes.", e);
}

// Extract the domain (ie: Windows / Active Directory domain) from the
// user's credentials
String domainName = getUserDomain(credentials);
necouchman marked this conversation as resolved.
Show resolved Hide resolved
if (domainName != null)
tokens.put(LDAP_DOMAIN_TOKEN, domainName);

return tokens;

}
Expand Down
Loading