Add .gitignore and .ratignore files for various directories
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
gyurix
2025-04-29 21:43:12 +02:00
parent 983ecbfc53
commit be9f66dee9
2167 changed files with 254128 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
target/
*~

View File

@@ -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.
#
dn: cn=Example Config,dc=guac-dev,dc=org
objectClass: guacConfigGroup
objectClass: groupOfNames
cn: Example Config
guacConfigProtocol: vnc
guacConfigParameter: hostname=localhost
guacConfigParameter: port=5900
guacConfigParameter: password=secret
member: cn=user1,dc=example,dc=com
member: cn=user2,dc=example,dc=com
seeAlso: cn=admins,ou=groups,dc=example,dc=com

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-auth-ldap</artifactId>
<packaging>jar</packaging>
<version>1.6.0</version>
<name>guacamole-auth-ldap</name>
<url>http://guacamole.apache.org/</url>
<parent>
<groupId>org.apache.guacamole</groupId>
<artifactId>extensions</artifactId>
<version>1.6.0</version>
<relativePath>../</relativePath>
</parent>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.6.0</version>
<scope>provided</scope>
</dependency>
<!-- Apache Directory LDAP API -->
<dependency>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-all</artifactId>
<version>2.1.7</version>
<exclusions>
<!--
Replace vulnerable version of Apache MINA until upstream
releases a version with fixed dependencies.
-->
<exclusion>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
</exclusion>
<!--
Replace slightly older commons-lang3 (3.15.0) with latest
compatible version (3.16.0) so that we don't need two copies
of the same license information.
-->
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<!-- Jackson and YAML support -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<!-- Apache Commons Lang (see exclusions for api-all) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.17.0</version>
</dependency>
<!-- Apache MINA (see exclusions for api-all) -->
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<version>2.2.4</version>
</dependency>
</dependencies>
</project>

View File

@@ -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.
#
dn: cn=guacConfigGroup,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: guacConfigGroup
olcAttributeTypes: ( 1.3.6.1.4.1.38971.1.1.1 NAME 'guacConfigProtocol'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( 1.3.6.1.4.1.38971.1.1.2 NAME 'guacConfigParameter'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( 1.3.6.1.4.1.38971.1.1.3 NAME 'guacConfigProxyHostname'
SINGLE-VALUE
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: ( 1.3.6.1.4.1.38971.1.1.4 NAME 'guacConfigProxyPort'
SINGLE-VALUE
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )
olcAttributeTypes: ( 1.3.6.1.4.1.38971.1.1.5 NAME 'guacConfigProxyEncryption'
SINGLE-VALUE
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcObjectClasses: ( 1.3.6.1.4.1.38971.1.2.1 NAME 'guacConfigGroup'
DESC 'Guacamole configuration group'
SUP groupOfNames
MUST guacConfigProtocol
MAY ( guacConfigParameter $
guacConfigProxyHostname $
guacConfigProxyPort $
guacConfigProxyEncryption ) )

View File

@@ -0,0 +1,46 @@
#
# 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.
#
attributetype ( 1.3.6.1.4.1.38971.1.1.1 NAME 'guacConfigProtocol'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 1.3.6.1.4.1.38971.1.1.2 NAME 'guacConfigParameter'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 1.3.6.1.4.1.38971.1.1.3 NAME 'guacConfigProxyHostname'
SINGLE-VALUE
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 1.3.6.1.4.1.38971.1.1.4 NAME 'guacConfigProxyPort'
SINGLE-VALUE
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )
attributetype ( 1.3.6.1.4.1.38971.1.1.5 NAME 'guacConfigProxyEncryption'
SINGLE-VALUE
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
objectClass ( 1.3.6.1.4.1.38971.1.2.1 NAME 'guacConfigGroup'
DESC 'Guacamole configuration group'
SUP groupOfNames
MUST guacConfigProtocol
MAY ( guacConfigParameter $
guacConfigProxyHostname $
guacConfigProxyPort $
guacConfigProxyEncryption ) )

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>dist</id>
<baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
<!-- Output tar.gz -->
<formats>
<format>tar.gz</format>
</formats>
<!-- Include docs, schema, and extension .jar -->
<fileSets>
<!-- Include docs -->
<fileSet>
<directory>doc</directory>
</fileSet>
<!-- Include schema -->
<fileSet>
<outputDirectory>schema</outputDirectory>
<directory>schema</directory>
</fileSet>
<!-- Include licenses -->
<fileSet>
<outputDirectory></outputDirectory>
<directory>target/licenses</directory>
</fileSet>
<!-- Include extension .jar -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,486 @@
/*
* 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;
import org.apache.guacamole.auth.ldap.user.UserLDAPConfiguration;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
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;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration;
import org.apache.guacamole.auth.ldap.group.UserGroupService;
import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
import org.apache.guacamole.auth.ldap.user.LDAPUserContext;
import org.apache.guacamole.auth.ldap.user.UserService;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.token.TokenName;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service providing convenience functions for the LDAP AuthenticationProvider
* implementation.
*/
public class AuthenticationProviderService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
/**
* The prefix that will be used when generating tokens.
*/
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
* 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.
*/
@Inject
private LDAPConnectionService ldapService;
/**
* Service for retrieving LDAP server configuration information.
*/
@Inject
private ConfigurationService confService;
/**
* Service for retrieving users and their corresponding LDAP DNs.
*/
@Inject
private UserService userService;
/**
* Service for retrieving user groups.
*/
@Inject
private UserGroupService userGroupService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<LDAPAuthenticatedUser> authenticatedUserProvider;
/**
* Provider for UserContext objects.
*/
@Inject
private Provider<LDAPUserContext> userContextProvider;
/**
* Determines the DN which corresponds to the user having the given
* username. The DN will either be derived directly from the user base DN,
* or queried from the LDAP server, depending on how LDAP authentication
* has been configured.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param username
* The username of the user whose corresponding DN should be returned.
*
* @return
* The DN which corresponds to the user having the given username.
*
* @throws GuacamoleException
* If required properties are missing, and thus the user DN cannot be
* determined.
*/
private Dn getUserBindDN(LDAPConfiguration config, String username)
throws GuacamoleException {
// If a search DN is provided, search the LDAP directory for the DN
// corresponding to the given username
String searchBindLogon = config.getSearchBindDN();
if (searchBindLogon != null) {
// Create an LDAP connection using the search account
LdapNetworkConnection searchConnection = ldapService.bindAs(config,
searchBindLogon, config.getSearchBindPassword());
// Warn of failure to find
if (searchConnection == null) {
logger.error("Unable to bind using search DN \"{}\"",
searchBindLogon);
return null;
}
try {
// Retrieve all DNs associated with the given username
List<Dn> userDNs = userService.getUserDNs(config, searchConnection, username);
if (userDNs.isEmpty())
return null;
// Warn if multiple DNs exist for the same user
if (userDNs.size() != 1) {
logger.warn("Multiple DNs possible for user \"{}\": {}", username, userDNs);
return null;
}
// Return the single possible DN
return userDNs.get(0);
}
// Always disconnect
finally {
searchConnection.close();
}
}
// Otherwise, derive user DN from base DN
return userService.deriveUserDN(config, username);
}
/**
* Returns a new UserLDAPConfiguration that is connected to an LDAP server
* associated with the Guacamole user having the given username and bound
* using the provided password. All LDAP servers associated with the given
* user are tried until the connection and authentication attempt succeeds.
* If no LDAP servers are available, or no LDAP servers are associated with
* the given user, null is returned. The Guacamole username will be
* internally translated to a fully-qualified LDAP DN according to the
* configuration of the LDAP server that is ultimately chosen.
*
* @param username
* The username of the Guacamole user to bind as.
*
* @param password
* The password of the user to bind as.
*
* @return
* A new UserLDAPConfiguration which is bound to an LDAP server using
* the provided credentials, or null if no LDAP servers are available
* for the given user or connecting/authenticating has failed.
*
* @throws GuacamoleException
* If configuration information for the user's LDAP server(s) cannot
* be retrieved.
*/
private UserLDAPConfiguration getLDAPConfiguration(String username,
String password) throws GuacamoleException {
// Get all LDAP server configurations
Collection<? extends LDAPConfiguration> configs = confService.getLDAPConfigurations();
if (configs.isEmpty()) {
logger.info("Skipping LDAP authentication as no LDAP servers are configured.");
return null;
}
// Try each possible LDAP configuration until the TCP connection and
// authentication are successful
for (LDAPConfiguration config : configs) {
// Attempt connection only if username matches
String translatedUsername = config.appliesTo(username);
if (translatedUsername == null) {
logger.debug("LDAP server \"{}\" does not match username \"{}\".", config.getServerHostname(), username);
continue;
}
logger.debug("LDAP server \"{}\" matched username \"{}\" as \"{}\".",
config.getServerHostname(), username, translatedUsername);
// Derive DN of user within this LDAP server
Dn bindDn = getUserBindDN(config, translatedUsername);
if (bindDn == null || bindDn.isEmpty()) {
logger.info("Unable to determine DN of user \"{}\" using LDAP "
+ "server \"{}\". Proceeding with next server...",
username, config.getServerHostname());
continue;
}
// Attempt bind (authentication)
LdapNetworkConnection ldapConnection = ldapService.bindAs(config, bindDn.getName(), password);
if (ldapConnection == null) {
logger.info("Unable to bind as user \"{}\" against LDAP "
+ "server \"{}\". Proceeding with next server...",
username, config.getServerHostname());
continue;
}
// Connection and bind were successful
logger.info("User \"{}\" was successfully authenticated by LDAP server \"{}\".", username, config.getServerHostname());
return new UserLDAPConfiguration(config, translatedUsername, bindDn, ldapConnection);
}
// No LDAP connection/authentication attempt succeeded
logger.info("User \"{}\" did not successfully authenticate against any LDAP server.", username);
return null;
}
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials. Also adds custom LDAP attributes to the
* AuthenticatedUser.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* An AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating the user, or if access is
* denied.
*/
public LDAPAuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Username and password are required
if (username == null
|| username.isEmpty()
|| password == null
|| password.isEmpty()) {
throw new GuacamoleInvalidCredentialsException(
"Anonymous bind is not currently allowed by the LDAP"
+ " authentication provider.", CredentialsInfo.USERNAME_PASSWORD);
}
UserLDAPConfiguration config = getLDAPConfiguration(username, password);
if (config == null)
throw new GuacamoleInvalidCredentialsException("Invalid login.",
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,
getUserTokens(config, credentials), effectiveGroups);
return authenticatedUser;
}
catch (GuacamoleException | RuntimeException | Error e) {
config.close();
throw e;
}
}
/**
* 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. 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> getUserTokens(ConnectedLDAPConfiguration config, Credentials credentials)
throws GuacamoleException {
// Get attributes from configuration information
Collection<String> attrList = config.getAttributes();
// If there are no attributes there is no reason to search LDAP
if (attrList.isEmpty())
return Collections.<String, String>emptyMap();
// Build LDAP query parameters
String[] attrArray = attrList.toArray(new String[attrList.size()]);
Map<String, String> tokens = new HashMap<>();
try {
// Get LDAP attributes by querying LDAP
Entry userEntry = config.getLDAPConnection().lookup(config.getBindDN(), attrArray);
if (userEntry == null)
return Collections.<String, String>emptyMap();
Collection<Attribute> attributes = userEntry.getAttributes();
if (attributes == null)
return Collections.<String, String>emptyMap();
// Convert each retrieved attribute into a corresponding token
for (Attribute attr : attributes) {
tokens.put(TokenName.canonicalize(attr.getId(),
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);
if (domainName != null)
tokens.put(LDAP_DOMAIN_TOKEN, domainName);
return tokens;
}
/**
* Returns a UserContext object initialized with data accessible to the
* given AuthenticatedUser.
*
* @param authenticatedUser
* The AuthenticatedUser to retrieve data for.
*
* @return
* A UserContext object initialized with data accessible to the given
* AuthenticatedUser.
*
* @throws GuacamoleException
* If the UserContext cannot be created due to an error.
*/
public LDAPUserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
if (authenticatedUser instanceof LDAPAuthenticatedUser) {
LDAPAuthenticatedUser ldapAuthenticatedUser = (LDAPAuthenticatedUser) authenticatedUser;
ConnectedLDAPConfiguration config = ldapAuthenticatedUser.getLDAPConfiguration();
try {
// Build user context by querying LDAP
LDAPUserContext userContext = userContextProvider.get();
userContext.init(ldapAuthenticatedUser);
return userContext;
}
// Always disconnect
finally {
config.close();
}
}
return null;
}
}

View File

@@ -0,0 +1,227 @@
/*
* 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;
import java.util.Collection;
import java.util.List;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.name.Dn;
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;
/**
* LDAPConfiguration implementation that is associated with an
* LdapNetworkConnection to the configured LDAP server.
*/
public class ConnectedLDAPConfiguration implements LDAPConfiguration, AutoCloseable {
/**
* The wrapped LDAPConfiguration.
*/
private final LDAPConfiguration config;
/**
* The connection to the LDAP server represented by this configuration.
*/
private final LdapNetworkConnection connection;
/**
* The LDAP DN that was used to bind with the LDAP server to produce
* {@link #connection}.
*/
private final Dn bindDn;
/**
* Creates a new ConnectedLDAPConfiguration that associates the given
* LdapNetworkConnection with the given LDAPConfiguration. All functions
* inherited from the LDAPConfiguration interface are delegated to the
* given LDAPConfiguration. It is the responsibility of the caller to
* ensure the provided LdapNetworkConnection is closed after it is no
* longer needed.
*
* @param config
* The LDAPConfiguration to wrap.
*
* @param bindDn
* The LDAP DN that was used to bind with the LDAP server to produce
* the given LdapNetworkConnection.
*
* @param connection
* The connection to the LDAP server represented by the given
* configuration.
*/
public ConnectedLDAPConfiguration(LDAPConfiguration config, Dn bindDn, LdapNetworkConnection connection) {
this.config = config;
this.bindDn = bindDn;
this.connection = connection;
}
/**
* Returns the LdapNetworkConnection for the connection to the LDAP server
* represented by this configuration. The lifecycle of this connection is
* managed externally. The connection is not guaranteed to still be
* connected.
*
* @return
* The LdapNetworkConnection for the connection to the LDAP server
* represented by this configuration.
*/
public LdapNetworkConnection getLDAPConnection() {
return connection;
}
/**
* Returns the LDAP DN that was used to bind with the LDAP server to
* produce the LdapNetworkConnection associated with this
* ConnectedLDAPConfiguration.
*
* @return
* The LDAP DN that was used to bind with the LDAP server.
*/
public Dn getBindDN() {
return bindDn;
}
@Override
public void close() {
connection.close();
}
@Override
public String appliesTo(String username) throws GuacamoleException {
return config.appliesTo(username);
}
@Override
public String getServerHostname() throws GuacamoleException {
return config.getServerHostname();
}
@Override
public int getServerPort() throws GuacamoleException {
return config.getServerPort();
}
@Override
public Collection<String> getUsernameAttributes() throws GuacamoleException {
return config.getUsernameAttributes();
}
@Override
public Dn getUserBaseDN() throws GuacamoleException {
return config.getUserBaseDN();
}
@Override
public Dn getConfigurationBaseDN() throws GuacamoleException {
return config.getConfigurationBaseDN();
}
@Override
public Collection<String> getGroupNameAttributes() throws GuacamoleException {
return config.getGroupNameAttributes();
}
@Override
public Dn getGroupBaseDN() throws GuacamoleException {
return config.getGroupBaseDN();
}
@Override
public String getSearchBindDN() throws GuacamoleException {
return config.getSearchBindDN();
}
@Override
public String getSearchBindPassword() throws GuacamoleException {
return config.getSearchBindPassword();
}
@Override
public EncryptionMethod getEncryptionMethod() throws GuacamoleException {
return config.getEncryptionMethod();
}
@Override
public LDAPSSLProtocol getSslProtocol() throws GuacamoleException {
return config.getSslProtocol();
}
@Override
public int getMaxResults() throws GuacamoleException {
return config.getMaxResults();
}
@Override
public AliasDerefMode getDereferenceAliases() throws GuacamoleException {
return config.getDereferenceAliases();
}
@Override
public boolean getFollowReferrals() throws GuacamoleException {
return config.getFollowReferrals();
}
@Override
public int getMaxReferralHops() throws GuacamoleException {
return config.getMaxReferralHops();
}
@Override
public ExprNode getUserSearchFilter() throws GuacamoleException {
return config.getUserSearchFilter();
}
@Override
public ExprNode getGroupSearchFilter() throws GuacamoleException {
return config.getGroupSearchFilter();
}
@Override
public int getOperationTimeout() throws GuacamoleException {
return config.getOperationTimeout();
}
@Override
public int getNetworkTimeout() throws GuacamoleException {
return config.getNetworkTimeout();
}
@Override
public Collection<String> getAttributes() throws GuacamoleException {
return config.getAttributes();
}
@Override
public String getMemberAttribute() throws GuacamoleException {
return config.getMemberAttribute();
}
@Override
public MemberAttributeType getMemberAttributeType() throws GuacamoleException {
return config.getMemberAttributeType();
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
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.TokenInjectingUserContext;
import org.apache.guacamole.net.auth.UserContext;
/**
* Allows users to be authenticated against an LDAP server. Each user may have
* any number of authorized configurations. Authorized configurations may be
* shared.
*/
public class LDAPAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* The identifier reserved for the root connection group.
*/
public static final String ROOT_CONNECTION_GROUP = "ROOT";
/**
* Injector which will manage the object graph of this authentication
* provider.
*/
private final Injector injector;
/**
* Creates a new LDAPAuthenticationProvider that authenticates users
* against an LDAP directory.
*
* @throws GuacamoleException
* If a required property is missing, or an error occurs while parsing
* a property.
*/
public LDAPAuthenticationProvider() throws GuacamoleException {
// Set up Guice injector.
injector = Guice.createInjector(
new LDAPAuthenticationProviderModule(this)
);
}
@Override
public String getIdentifier() {
return "ldap";
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException {
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.getUserContext(authenticatedUser);
}
@Override
public UserContext decorate(UserContext context,
AuthenticatedUser authenticatedUser, Credentials credentials)
throws GuacamoleException {
// Only decorate if the user authenticated against LDAP
if (!(authenticatedUser instanceof LDAPAuthenticatedUser))
return context;
// Apply LDAP-specific tokens to all connections / connection groups
return new TokenInjectingUserContext(context,
((LDAPAuthenticatedUser) authenticatedUser).getTokens());
}
}

View File

@@ -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;
import com.google.inject.AbstractModule;
import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.auth.ldap.connection.ConnectionService;
import org.apache.guacamole.auth.ldap.user.UserService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.group.UserGroupService;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
/**
* Guice module which configures LDAP-specific injections.
*/
public class LDAPAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* A reference to the LDAPAuthenticationProvider on behalf of which this
* module has configured injection.
*/
private final AuthenticationProvider authProvider;
/**
* Creates a new LDAP authentication provider module which configures
* injection for the LDAPAuthenticationProvider.
*
* @param authProvider
* The AuthenticationProvider for which injection is being configured.
*
* @throws GuacamoleException
* If an error occurs while retrieving the Guacamole server
* environment.
*/
public LDAPAuthenticationProviderModule(AuthenticationProvider authProvider)
throws GuacamoleException {
// Get local environment
this.environment = LocalEnvironment.getInstance();
// Store associated auth provider
this.authProvider = authProvider;
}
@Override
protected void configure() {
// Bind core implementations of guacamole-ext classes
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind LDAP-specific services
bind(ConfigurationService.class);
bind(ConnectionService.class);
bind(LDAPConnectionService.class);
bind(ObjectQueryService.class);
bind(UserGroupService.class);
bind(UserService.class);
}
}

View File

@@ -0,0 +1,478 @@
/*
* 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;
import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.url.LdapUrl;
import org.apache.directory.ldap.client.api.LdapConnectionConfig;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.GuacamoleException;
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;
/**
* Service for creating and managing connections to LDAP servers.
*/
public class LDAPConnectionService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
/**
* 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 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
* 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 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
* 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, int timeout)
throws GuacamoleException {
return createLDAPConnection(host, port, encryptionMethod,
LDAPSSLProtocol.TLSv1_3, timeout);
}
/**
* Creates a new instance of LdapNetworkConnection, configured as required
* to use whichever encryption method, hostname, and port are requested
* within guacamole.properties. 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 config
* The configuration of the LDAP server being queried.
*
* @return
* A new LdapNetworkConnection instance which has already been
* configured to use the encryption method, hostname, and port
* requested within guacamole.properties.
*
* @throws GuacamoleException
* If an error occurs while parsing guacamole.properties, or if the
* requested encryption method is actually not implemented (a bug).
*/
private LdapNetworkConnection createLDAPConnection(LDAPConfiguration config)
throws GuacamoleException {
return createLDAPConnection(
config.getServerHostname(),
config.getServerPort(),
config.getEncryptionMethod(),
config.getSslProtocol(),
config.getNetworkTimeout());
}
/**
* Creates a new instance of LdapNetworkConnection, configured as required
* to use whichever encryption method, hostname, and port are specified
* within the given LDAP URL. 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 config
* The configuration of the LDAP server being queried.
*
* @param url
* The LDAP URL containing the details which should be used to connect
* to the LDAP server.
*
* @return
* A new LdapNetworkConnection instance which has already been
* configured to use the encryption method, hostname, and port
* specified within the given LDAP URL.
*
* @throws GuacamoleException
* If the given URL is not a valid LDAP URL, or if the encryption
* method indicated by the URL is known but not actually implemented (a
* bug).
*/
private LdapNetworkConnection createLDAPConnection(LDAPConfiguration config,
String url) throws GuacamoleException {
// Parse provided LDAP URL
LdapUrl ldapUrl;
try {
ldapUrl = new LdapUrl(url);
}
catch (LdapException e) {
logger.debug("Cannot connect to LDAP URL \"{}\": URL is invalid.", url, e);
throw new GuacamoleServerException("Invalid LDAP URL.", e);
}
// Retrieve hostname from URL, bailing out if no hostname is present
String host = ldapUrl.getHost();
if (host == null || host.isEmpty()) {
logger.debug("Cannot connect to LDAP URL \"{}\": no hostname is present.", url);
throw new GuacamoleServerException("LDAP URL contains no hostname.");
}
// Parse encryption method from URL scheme
EncryptionMethod encryptionMethod = EncryptionMethod.NONE;
if (LdapUrl.LDAPS_SCHEME.equals(ldapUrl.getScheme()))
encryptionMethod = EncryptionMethod.SSL;
// Use STARTTLS for otherwise unencrypted ldap:// URLs if the main
// LDAP connection requires STARTTLS
else if (config.getEncryptionMethod() == EncryptionMethod.STARTTLS) {
logger.debug("Using STARTTLS for LDAP URL \"{}\" as the main LDAP "
+ "connection described in guacamole.properties is "
+ "configured to use STARTTLS.", url);
encryptionMethod = EncryptionMethod.STARTTLS;
}
// If no post is specified within the URL, use the default port
// dictated by the encryption method
int port = ldapUrl.getPort();
if (port < 1)
port = encryptionMethod.DEFAULT_PORT;
return createLDAPConnection(host, port, encryptionMethod,
config.getSslProtocol(), config.getNetworkTimeout());
}
/**
* Binds to the LDAP server indicated by the given LdapNetworkConnection
* using the given credentials. If the LdapNetworkConnection is not yet
* connected, an LDAP connection is first established. The provided
* credentials will be stored within the LdapConnectionConfig of the given
* LdapNetworkConnection. If the bind operation fails, the given
* LdapNetworkConnection is automatically closed.
*
* @param ldapConnection
* The LdapNetworkConnection describing the connection to the LDAP
* server. This LdapNetworkConnection is modified as a result of this
* call and will be automatically closed if this call fails.
*
* @param userDN
* The DN of the user to bind as, or null to bind anonymously.
*
* @param password
* The password to use when binding as the specified user, or null to
* attempt to bind without a password.
*
* @return
* A bound LDAP connection, or null if the connection could not be
* bound.
*/
private LdapNetworkConnection bindAs(LdapNetworkConnection ldapConnection,
String bindUser, String password) {
// Add credentials to existing config
LdapConnectionConfig config = ldapConnection.getConfig();
config.setName(bindUser);
config.setCredentials(password);
try {
// Connect and bind using provided credentials
ldapConnection.bind();
}
// Disconnect if an authentication error occurs, but log that failure
// only at the debug level (such failures are expected)
catch (LdapAuthenticationException e) {
ldapConnection.close();
logger.debug("Bind attempt with LDAP server as user \"{}\" failed.",
bindUser, e);
return null;
}
// Disconnect for all other bind failures, as well, logging those at
// the error level
catch (LdapException e) {
ldapConnection.close();
logger.error("Binding with the LDAP server at \"{}\" as user "
+ "\"{}\" failed: {}", config.getLdapHost(), bindUser,
e.getMessage());
logger.debug("Unable to bind to LDAP server.", e);
return null;
}
return ldapConnection;
}
/**
* Binds to the LDAP server indicated by a given LdapNetworkConnection
* using the credentials that were used to bind another
* LdapNetworkConnection. If the LdapNetworkConnection about to be bound is
* not yet connected, an LDAP connection is first established. The
* credentials from the other LdapNetworkConnection will be stored within
* the LdapConnectionConfig of the given LdapNetworkConnection. If the bind
* operation fails, the given LdapNetworkConnection is automatically
* closed.
*
* @param ldapConnection
* The LdapNetworkConnection describing the connection to the LDAP
* server. This LdapNetworkConnection is modified as a result of this
* call and will be automatically closed if this call fails.
*
* @param useCredentialsFrom
* A bound LdapNetworkConnection whose bind credentials should be
* copied for use within this bind operation.
*
* @return
* A bound LDAP connection, or null if the connection could not be
* bound.
*/
private LdapNetworkConnection bindAs(LdapNetworkConnection ldapConnection,
LdapNetworkConnection useCredentialsFrom) {
// Copy bind username and password from original config
LdapConnectionConfig ldapConfig = useCredentialsFrom.getConfig();
String username = ldapConfig.getName();
String password = ldapConfig.getCredentials();
// Parse bind username as an LDAP DN
Dn userDN;
try {
userDN = new Dn(username);
}
catch (LdapInvalidDnException e) {
logger.error("Credentials of existing connection cannot be used. "
+ "The username used (\"{}\") is not a valid DN.", username);
logger.debug("Cannot bind using invalid DN.", e);
ldapConnection.close();
return null;
}
// Bind using username/password from existing connection
return bindAs(ldapConnection, userDN.getName(), password);
}
/**
* Binds to the LDAP server using the provided user DN and password. The
* hostname, port, and encryption method of the LDAP server are determined
* from guacamole.properties.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param bindUser
* The DN or UPN of the user to bind as, or null to bind anonymously.
*
* @param password
* The password to use when binding as the specified user, or null to
* attempt to bind without a password.
*
* @return
* A bound LDAP connection, or null if the connection could not be
* bound.
*
* @throws GuacamoleException
* If an error occurs while parsing guacamole.properties, or if the
* configured encryption method is actually not implemented (a bug).
*/
public LdapNetworkConnection bindAs(LDAPConfiguration config,
String bindUser, String password) throws GuacamoleException {
return bindAs(createLDAPConnection(config), bindUser, password);
}
/**
* Binds to the LDAP server indicated by the given LDAP URL using the
* credentials that were used to bind an existing LdapNetworkConnection.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param url
* The LDAP URL containing the details which should be used to connect
* to the LDAP server.
*
* @param useCredentialsFrom
* A bound LdapNetworkConnection whose bind credentials should be
* copied for use within this bind operation.
*
* @return
* A bound LDAP connection, or null if the connection could not be
* bound.
*
* @throws GuacamoleException
* If the given URL is not a valid LDAP URL, or if the encryption
* method indicated by the URL is known but not actually implemented (a
* bug).
*/
public LdapNetworkConnection bindAs(LDAPConfiguration config, String url,
LdapNetworkConnection useCredentialsFrom)
throws GuacamoleException {
return bindAs(createLDAPConnection(config, url), useCredentialsFrom);
}
/**
* Generate a SearchRequest object using the given Base DN and filter
* and retrieving other properties from the LDAP configuration service.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param baseDn
* The LDAP Base DN at which to search the search.
*
* @param filter
* A string representation of a LDAP filter to use for the search.
*
* @return
* The properly-configured SearchRequest object.
*
* @throws GuacamoleException
* If an error occurs retrieving any of the configuration values.
*/
public SearchRequest getSearchRequest(LDAPConfiguration config, Dn baseDn,
ExprNode filter) throws GuacamoleException {
SearchRequest searchRequest = new SearchRequestImpl();
searchRequest.setBase(baseDn);
searchRequest.setDerefAliases(config.getDereferenceAliases());
searchRequest.setScope(SearchScope.SUBTREE);
searchRequest.setFilter(filter);
searchRequest.setSizeLimit(config.getMaxResults());
searchRequest.setTimeLimit(config.getOperationTimeout());
searchRequest.setTypesOnly(false);
if (config.getFollowReferrals())
searchRequest.followReferrals();
return searchRequest;
}
}

View File

@@ -0,0 +1,397 @@
/*
* 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;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.SearchCursor;
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;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.filter.AndNode;
import org.apache.directory.api.ldap.model.filter.EqualityNode;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.OrNode;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration;
import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties;
import org.apache.guacamole.net.auth.Identifiable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for executing queries against an LDAP directory intended to retrieve
* Guacamole-related objects. Referrals are automatically handled. Convenience
* functions are provided for generating the LDAP queries typically required
* for retrieving Guacamole objects, as well as for converting the results of a
* query into a {@link Map} of Guacamole objects.
*/
public class ObjectQueryService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
/**
* Service for connecting to LDAP directory.
*/
@Inject
private LDAPConnectionService ldapService;
/**
* Returns the identifier of the object represented by the given LDAP
* entry. Multiple attributes may be declared as containing the identifier
* of the object when present on an LDAP entry. If multiple such attributes
* are present on the same LDAP entry, the value of the attribute with
* highest priority is used. If multiple copies of the same attribute are
* present on the same LDAPentry, the first value of that attribute is
* used.
*
* @param entry
* The entry representing the Guacamole object whose unique identifier
* should be determined.
*
* @param attributes
* A collection of all attributes which may be used to specify the
* unique identifier of the Guacamole object represented by an LDAP
* entry, in order of decreasing priority.
*
* @return
* The identifier of the object represented by the given LDAP entry, or
* null if no attributes declared as containing the identifier of the
* object are present on the entry.
*
* @throws LdapInvalidAttributeValueException
* If an error occurs retrieving the value of the identifier attribute.
*/
public String getIdentifier(Entry entry, Collection<String> attributes)
throws LdapInvalidAttributeValueException {
// Retrieve the first value of the highest priority identifier attribute
for (String identifierAttribute : attributes) {
Attribute identifier = entry.get(identifierAttribute);
if (identifier != null)
return identifier.getString();
}
// No identifier attribute is present on the entry
return null;
}
/**
* Generates a properly-escaped LDAP query which finds all objects which
* match the given LDAP filter and which have at least one of the given
* attributes set to the specified value.
*
* @param filter
* The LDAP filter to apply to reduce the results of the query in
* addition to testing the values of the given attributes.
*
* @param attributes
* A collection of all attributes to test for equivalence to the given
* value, in order of decreasing priority.
*
* @param attributeValue
* The value that the resulting LDAP query should search for within the
* attributes of objects within the LDAP directory. If null, the
* resulting LDAP query will search for the presence of at least one of
* the given attributes on each object, regardless of the value of
* those attributes.
*
* @return
* An LDAP query which will search for arbitrary LDAP objects having at
* least one of the given attributes set to the specified value.
*/
public ExprNode generateQuery(ExprNode filter,
Collection<String> attributes, String attributeValue) {
// Build LDAP query for objects having at least one attribute and with
// the given search filter
AndNode searchFilter = new AndNode();
searchFilter.addNode(filter);
// If no attributes provided, we're done.
if (attributes.size() < 1)
return searchFilter;
// Include all attributes within OR clause
OrNode attributeFilter = new OrNode();
// If value is defined, check each attribute for that value.
if (attributeValue != null) {
attributes.forEach(attribute ->
attributeFilter.addNode(new EqualityNode(attribute,
attributeValue))
);
}
// If no value is defined, just check for presence of attribute.
else {
attributes.forEach(attribute ->
attributeFilter.addNode(new PresenceNode(attribute))
);
}
searchFilter.addNode(attributeFilter);
logger.trace("Sending LDAP filter: \"{}\"", searchFilter.toString());
return searchFilter;
}
/**
* Executes an arbitrary LDAP query using the given connection, returning a
* list of all results. Only objects beneath the given base DN are
* included in the search.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param ldapConnection
* The current connection to the LDAP server, associated with the
* current user.
*
* @param baseDN
* The base DN to search using the given LDAP query.
*
* @param query
* The LDAP query to execute.
*
* @param searchHop
* The current level of referral depth for this search, used for
* limiting the maximum depth to which referrals can go.
*
* @param attributes
* A collection of the names of attributes that should be retrieved
* from LDAP entries returned by the search, or null if all available
* attributes should be returned.
*
* @return
* A list of all results accessible to the user currently bound under
* the given LDAP connection.
*
* @throws GuacamoleException
* If an error occurs executing the query, or if configuration
* information required to execute the query cannot be read from
* guacamole.properties.
*/
public List<Entry> search(LDAPConfiguration config,
LdapNetworkConnection ldapConnection, Dn baseDN, ExprNode query,
int searchHop, Collection<String> attributes)
throws GuacamoleException {
// Refuse to follow referrals if limit has been reached
int maxHops = config.getMaxReferralHops();
if (searchHop >= maxHops) {
logger.debug("Refusing to follow further referrals as the maximum "
+ "number of referral hops ({}) has been reached. LDAP "
+ "search results may be incomplete. If further referrals "
+ "should be followed, consider setting the \"{}\" "
+ "property to a larger value.", maxHops, LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS.getName());
return Collections.emptyList();
}
logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query);
// Search within subtree of given base DN
SearchRequest request = ldapService.getSearchRequest(config, baseDN, query);
if (attributes != null)
request.addAttributes(attributes.toArray(new String[0]));
// Produce list of all entries in the search result, automatically
// following referrals if configured to do so
List<Entry> entries = new ArrayList<>();
try (SearchCursor results = ldapConnection.search(request)) {
while (results.next()) {
// Add entry directly if no referral is involved
if (results.isEntry())
entries.add(results.getEntry());
// If a referral must be followed to obtain further results,
// retrieval of those results depends on whether such referral
// following is enabled
else if (results.isReferral()) {
// Follow received referrals only if configured to do so
if (request.isFollowReferrals()) {
for (String url : results.getReferral().getLdapUrls()) {
// Connect to referred LDAP server to retrieve further results, ensuring the network
// connection is always closed when it will no longer be used
try (LdapNetworkConnection referralConnection = ldapService.bindAs(config, url, ldapConnection)) {
if (referralConnection != null) {
logger.debug("Following referral to \"{}\"...", url);
entries.addAll(search(config, referralConnection, baseDN, query, searchHop + 1, attributes));
}
else
logger.debug("Could not bind with LDAP "
+ "server indicated by referral "
+ "URL \"{}\".", url);
}
catch (GuacamoleException e) {
logger.warn("Referral to \"{}\" could not be followed: {}", url, e.getMessage());
logger.debug("Failed to follow LDAP referral.", e);
}
}
}
// Log if referrals may be applicable but they aren't being
// followed
else
logger.debug("Referrals to one or more other LDAP "
+ "servers were received but are being "
+ "ignored because following of referrals is "
+ "not enabled. If referrals must be "
+ "followed, consider setting the \"{}\" "
+ "property to \"true\".", LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS.getName());
}
}
return entries;
}
catch (CursorException | IOException | LdapException e) {
throw new GuacamoleServerException("Unable to query list of "
+ "objects from LDAP directory: " + e.getMessage(), e);
}
}
/**
* Executes the query which would be returned by generateQuery() using the
* given connection, returning a list of all results. Only objects beneath
* the given base DN are included in the search.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param ldapConnection
* The current connection to the LDAP server, associated with the
* current user.
*
* @param baseDN
* The base DN to search using the given LDAP query.
*
* @param filter
* The LDAP filter to apply to reduce the results of the query in
* addition to testing the values of the given attributes.
*
* @param filterAttributes
* A collection of all attributes to test for equivalence to the given
* value, in order of decreasing priority.
*
* @param filterValue
* The value that should be searched search for within the attributes
* of objects within the LDAP directory. If null, the search will test
* only for the presence of at least one of the given attributes on
* each object, regardless of the value of those attributes.
*
* @param attributes
* A collection of the names of attributes that should be retrieved
* from LDAP entries returned by the search, or null if all available
* attributes should be returned.
*
* @return
* A list of all results accessible to the user currently bound under
* the given LDAP connection.
*
* @throws GuacamoleException
* If an error occurs executing the query, or if configuration
* information required to execute the query cannot be read from
* guacamole.properties.
*/
public List<Entry> search(LDAPConfiguration config,
LdapNetworkConnection ldapConnection, Dn baseDN, ExprNode filter,
Collection<String> filterAttributes, String filterValue,
Collection<String> attributes) throws GuacamoleException {
ExprNode query = generateQuery(filter, filterAttributes, filterValue);
return search(config, ldapConnection, baseDN, query, 0, attributes);
}
/**
* Converts a given list of LDAP entries to a {@link Map} of Guacamole
* objects stored by their identifiers.
*
* @param <ObjectType>
* The type of object to store within the {@link Map}.
*
* @param entries
* A list of LDAP entries to convert to Guacamole objects.
*
* @param mapper
* A mapping function which converts a given LDAP entry to its
* corresponding Guacamole object. If the LDAP entry cannot be
* converted, null should be returned.
*
* @return
* A new {@link Map} containing Guacamole object versions of each of
* the given LDAP entries, where each object is stored within the
* {@link Map} under its corresponding identifier.
*/
public <ObjectType extends Identifiable> Map<String, ObjectType>
asMap(List<Entry> entries, Function<Entry, ObjectType> mapper) {
// Convert each entry to the corresponding Guacamole API object
Map<String, ObjectType> objects = new HashMap<>(entries.size());
for (Entry entry : entries) {
ObjectType object = mapper.apply(entry);
if (object == null) {
logger.debug("Ignoring object \"{}\".", entry.getDn().toString());
continue;
}
// Attempt to add object to map, warning if the object appears
// to be a duplicate
String identifier = object.getIdentifier();
if (objects.putIfAbsent(identifier, object) != null)
logger.warn("Multiple objects ambiguously map to the "
+ "same identifier (\"{}\"). Ignoring \"{}\" as "
+ "a duplicate.", identifier, entry.getDn().toString());
}
return objects;
}
}

View File

@@ -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.ldap.conf;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import com.fasterxml.jackson.databind.type.LogicalType;
import java.io.IOException;
import java.io.Serializable;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Custom JSON (or YAML) deserializer for Jackson that deserializes string
* values as Patterns with the case insensitive flag set by default. Jackson
* will actually handle deserialization of Patterns automatically, but does not
* provide for setting the default flags.
*/
public class CaseInsensitivePatternDeserializer extends StdScalarDeserializer<Pattern> {
/**
* Unique version identifier of this {@link Serializable} class.
*/
private static final long serialVersionUID = 1L;
/**
* Creates a new CaseInsensitivePatternDeserializer which deserializes
* string values to Pattern objects with the case insensitive flag set.
*/
public CaseInsensitivePatternDeserializer() {
super(Pattern.class);
}
@Override
public LogicalType logicalType() {
return LogicalType.Textual;
}
@Override
public boolean isCachable() {
return true;
}
@Override
public Pattern deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
if (!parser.hasToken(JsonToken.VALUE_STRING))
throw new JsonParseException(parser, "Regular expressions may only be represented as strings.");
try {
return Pattern.compile(parser.getText(), Pattern.CASE_INSENSITIVE);
}
catch (PatternSyntaxException e) {
throw new JsonParseException(parser, "Invalid regular expression.", e);
}
}
}

View File

@@ -0,0 +1,152 @@
/*
* 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 com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for retrieving configuration information regarding LDAP servers.
*/
@Singleton
public class ConfigurationService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ConfigurationService.class);
/**
* ObjectMapper for deserializing YAML.
*/
private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory())
.registerModule(new SimpleModule().addDeserializer(Pattern.class, new CaseInsensitivePatternDeserializer()));
/**
* The name of the file within GUACAMOLE_HOME that defines each available
* LDAP server (if not using guacamole.properties).
*/
private static final String LDAP_SERVERS_YML = "ldap-servers.yml";
/**
* The timestamp that the {@link #LDAP_SERVERS_YML} was last modified when
* it was read, as would be returned by {@link File#lastModified()}.
*/
private final AtomicLong lastModified = new AtomicLong(0);
/**
* The cached copy of the configuration read from {@link #LDAP_SERVERS_YML}.
* If the current set of LDAP servers has not yet been read from the YAML
* configuration file, or if guacamole.properties is being used instead,
* this will be null.
*/
private Collection<JacksonLDAPConfiguration> cachedConfigurations = null;
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* Returns the configuration information for all configured LDAP servers.
* If multiple servers are returned, each should be tried in order until a
* successful LDAP connection is established.
*
* @return
* The configurations of all LDAP servers.
*
* @throws GuacamoleException
* If the configuration information of the LDAP servers cannot be
* retrieved due to an error.
*/
public Collection<? extends LDAPConfiguration> getLDAPConfigurations() throws GuacamoleException {
// Read/refresh configuration from YAML, if available
File ldapServers = new File(environment.getGuacamoleHome(), LDAP_SERVERS_YML);
if (ldapServers.exists()) {
long oldLastModified = lastModified.get();
long currentLastModified = ldapServers.lastModified();
// Update cached copy of YAML if things have changed, ensuring only
// one concurrent request updates the cache at any given time
if (currentLastModified > oldLastModified && lastModified.compareAndSet(oldLastModified, currentLastModified)) {
try {
logger.debug("Reading updated LDAP configuration from \"{}\"...", ldapServers);
Collection<JacksonLDAPConfiguration> configs = mapper.readValue(ldapServers, new TypeReference<Collection<JacksonLDAPConfiguration>>() {});
if (configs != null) {
logger.debug("Reading LDAP configuration defaults from guacamole.properties...");
LDAPConfiguration defaultConfig = new EnvironmentLDAPConfiguration(environment);
configs.forEach((config) -> config.setDefaults(defaultConfig));
}
else
logger.debug("Using only guacamole.properties for "
+ "LDAP server definitions as \"{}\" is "
+ "empty.", ldapServers);
cachedConfigurations = configs;
}
catch (IOException e) {
logger.error("\"{}\" could not be read/parsed: {}", ldapServers, e.getMessage());
}
}
else
logger.debug("Using cached LDAP configuration from \"{}\".", ldapServers);
}
// Clear cached YAML if it no longer exists
else if (cachedConfigurations != null) {
long oldLastModified = lastModified.get();
if (lastModified.compareAndSet(oldLastModified, 0)) {
logger.debug("Clearing cached LDAP configuration from \"{}\" (file no longer exists).", ldapServers);
cachedConfigurations = null;
}
}
// Use guacamole.properties if not using YAML
if (cachedConfigurations == null) {
logger.debug("Reading LDAP configuration from guacamole.properties...");
return Collections.singletonList(new EnvironmentLDAPConfiguration(environment));
}
return cachedConfigurations;
}
}

View File

@@ -0,0 +1,154 @@
/*
* 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 java.util.Collections;
import java.util.List;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
/**
* LDAPConfiguration implementation that returns the default values for all
* configuration parameters. For any configuration parameters that are
* required (such as {@link #getUserBaseDN()}), an exception is thrown.
*/
public class DefaultLDAPConfiguration implements LDAPConfiguration {
@Override
public String appliesTo(String username) {
return null;
}
@Override
public String getServerHostname() {
return "localhost";
}
@Override
public int getServerPort() {
return getEncryptionMethod().DEFAULT_PORT;
}
@Override
public List<String> getUsernameAttributes() {
return Collections.singletonList("uid");
}
@Override
public Dn getUserBaseDN() throws GuacamoleException {
throw new GuacamoleServerException("All LDAP servers must have a defined user base DN.");
}
@Override
public Dn getConfigurationBaseDN() {
return null;
}
@Override
public List<String> getGroupNameAttributes() {
return Collections.singletonList("cn");
}
@Override
public Dn getGroupBaseDN() {
return null;
}
@Override
public String getSearchBindDN() {
return null;
}
@Override
public String getSearchBindPassword() {
return null;
}
@Override
public EncryptionMethod getEncryptionMethod() {
return EncryptionMethod.NONE;
}
@Override
public LDAPSSLProtocol getSslProtocol() {
return LDAPSSLProtocol.TLSv1_3;
}
@Override
public int getMaxResults() {
return 1000;
}
@Override
public AliasDerefMode getDereferenceAliases() {
return AliasDerefMode.NEVER_DEREF_ALIASES;
}
@Override
public boolean getFollowReferrals() {
return false;
}
@Override
public int getMaxReferralHops() {
return 5;
}
@Override
public ExprNode getUserSearchFilter() {
return new PresenceNode("objectClass");
}
@Override
public ExprNode getGroupSearchFilter() {
return new PresenceNode("objectClass");
}
@Override
public int getOperationTimeout() {
return 30;
}
@Override
public int getNetworkTimeout() {
return 30000;
}
@Override
public List<String> getAttributes() {
return Collections.<String>emptyList();
}
@Override
public String getMemberAttribute() {
return "member";
}
@Override
public MemberAttributeType getMemberAttributeType()
throws GuacamoleException {
return MemberAttributeType.DN;
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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 encryption methods which may be used when connecting to an LDAP
* server.
*/
public enum EncryptionMethod {
/**
* No encryption will be used. All data will be sent to the LDAP server in
* plaintext. Unencrypted LDAP connections use port 389 by default.
*/
@PropertyValue("none")
NONE(389),
/**
* The connection to the LDAP server will be encrypted with SSL. LDAP over
* SSL (LDAPS) will use port 636 by default.
*/
@PropertyValue("ssl")
SSL(636),
/**
* The connection to the LDAP server will be encrypted using STARTTLS. TLS
* connections are negotiated over the standard LDAP port of 389 - the same
* port used for unencrypted traffic.
*/
@PropertyValue("starttls")
STARTTLS(389);
/**
* The default port of this specific encryption method. As with most
* protocols, the default port for LDAP varies by whether SSL is used.
*/
public final int DEFAULT_PORT;
/**
* Initializes this encryption method such that it is associated with the
* given default port.
*
* @param defaultPort
* The default port to associate with this encryption method.
*/
private EncryptionMethod(int defaultPort) {
this.DEFAULT_PORT = defaultPort;
}
}

View File

@@ -0,0 +1,237 @@
/*
* 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 java.util.Collection;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
/**
* LDAPConfiguration implementation that reads its configuration details from
* guacamole.properties.
*/
public class EnvironmentLDAPConfiguration implements LDAPConfiguration {
/**
* The Guacamole server environment.
*/
private final Environment environment;
/**
* The default configuration options for all parameters.
*/
private static final LDAPConfiguration DEFAULT = new DefaultLDAPConfiguration();
/**
* Creates a new EnvironmentLDAPConfiguration that reads its configuration
* details from guacamole.properties, as exposed by the given Environment.
*
* @param environment
* The Guacamole server environment.
*/
public EnvironmentLDAPConfiguration(Environment environment) {
this.environment = environment;
}
@Override
public String appliesTo(String username) throws GuacamoleException {
return username;
}
@Override
public String getServerHostname() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_HOSTNAME,
DEFAULT.getServerHostname()
);
}
@Override
public int getServerPort() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_PORT,
getEncryptionMethod().DEFAULT_PORT
);
}
@Override
public Collection<String> getUsernameAttributes() throws GuacamoleException {
return environment.getPropertyCollection(
LDAPGuacamoleProperties.LDAP_USERNAME_ATTRIBUTE,
DEFAULT.getUsernameAttributes()
);
}
@Override
public Dn getUserBaseDN() throws GuacamoleException {
return environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USER_BASE_DN
);
}
@Override
public Dn getConfigurationBaseDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN,
DEFAULT.getConfigurationBaseDN()
);
}
@Override
public Collection<String> getGroupNameAttributes() throws GuacamoleException {
return environment.getPropertyCollection(
LDAPGuacamoleProperties.LDAP_GROUP_NAME_ATTRIBUTE,
DEFAULT.getGroupNameAttributes()
);
}
@Override
public Dn getGroupBaseDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN,
DEFAULT.getGroupBaseDN()
);
}
@Override
public String getSearchBindDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN,
DEFAULT.getSearchBindDN()
);
}
@Override
public String getSearchBindPassword() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_SEARCH_BIND_PASSWORD,
DEFAULT.getSearchBindPassword()
);
}
@Override
public EncryptionMethod getEncryptionMethod() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_ENCRYPTION_METHOD,
DEFAULT.getEncryptionMethod()
);
}
@Override
public LDAPSSLProtocol getSslProtocol() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_SSL_PROTOCOL,
DEFAULT.getSslProtocol()
);
}
@Override
public int getMaxResults() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS,
DEFAULT.getMaxResults()
);
}
@Override
public AliasDerefMode getDereferenceAliases() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES,
DEFAULT.getDereferenceAliases()
);
}
@Override
public boolean getFollowReferrals() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS,
DEFAULT.getFollowReferrals()
);
}
@Override
public int getMaxReferralHops() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS,
DEFAULT.getMaxReferralHops()
);
}
@Override
public ExprNode getUserSearchFilter() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER,
DEFAULT.getUserSearchFilter()
);
}
@Override
public ExprNode getGroupSearchFilter() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER,
DEFAULT.getGroupSearchFilter()
);
}
@Override
public int getOperationTimeout() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_OPERATION_TIMEOUT,
DEFAULT.getOperationTimeout()
);
}
@Override
public int getNetworkTimeout() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_NETWORK_TIMEOUT,
DEFAULT.getNetworkTimeout()
);
}
@Override
public Collection<String> getAttributes() throws GuacamoleException {
return environment.getPropertyCollection(
LDAPGuacamoleProperties.LDAP_USER_ATTRIBUTES,
DEFAULT.getAttributes()
);
}
@Override
public String getMemberAttribute() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE,
DEFAULT.getMemberAttribute()
);
}
@Override
public MemberAttributeType getMemberAttributeType()
throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE,
DEFAULT.getMemberAttributeType()
);
}
}

View File

@@ -0,0 +1,443 @@
/*
* 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 com.fasterxml.jackson.annotation.JsonFormat;
import static com.fasterxml.jackson.annotation.JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* LDAPConfiguration implementation that is annotated for deserialization by
* Jackson.
*/
public class JacksonLDAPConfiguration implements LDAPConfiguration {
/**
* The regular expressions that match all users that should be routed to
* the LDAP server represented by this configuration.
*/
@JsonProperty("match-usernames")
@JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<Pattern> matchUsernames;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_HOSTNAME}. If
* not set within the YAML, this will be null.
*/
@JsonProperty("hostname")
private String hostname;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_PORT}. If not
* set within the YAML, this will be null.
*/
@JsonProperty("port")
private Integer port;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USERNAME_ATTRIBUTES}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("username-attribute")
@JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<String> usernameAttributes;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USER_BASE_DN}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("user-base-dn")
private String userBaseDn;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_CONFIG_BASE_DN}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("config-base-dn")
private String configBaseDn;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_GROUP_BASE_DN}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("group-base-dn")
private String groupBaseDn;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_GROUP_NAME_ATTRIBUTES}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("group-name-attribute")
@JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<String> groupNameAttributes;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_SEARCH_BIND_DN}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("search-bind-dn")
private String searchBindDn;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_SEARCH_BIND_PASSWORD}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("search-bind-password")
private String searchBindPassword;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_ENCRYPTION_METHOD}.
* If not set within the YAML, this will be null.
*/
@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}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("max-search-results")
private Integer maxSearchResults;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_DEREFERENCE_ALIASES}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("dereference-aliases")
private String dereferenceAliases;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_FOLLOW_REFERRALS}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("follow-referrals")
private Boolean followReferrals;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MAX_REFERRAL_HOPS}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("max-referral-hops")
private Integer maxReferralHops;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USER_SEARCH_FILTER}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("user-search-filter")
private String userSearchFilter;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_GROUP_SEARCH_FILTER}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("group-search-filter")
private String groupSearchFilter;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_OPERATION_TIMEOUT}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("operation-timeout")
private Integer operationTimeout;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_NETWORK_TIMEOUT}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("network-timeout")
private Integer networkTimeout;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_USER_ATTRIBUTES}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("user-attributes")
@JsonFormat(with = ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<String> userAttributes;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MEMBER_ATTRIBUTE}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("member-attribute")
private String memberAttribute;
/**
* The raw YAML value of {@link LDAPGuacamoleProperties#LDAP_MEMBER_ATTRIBUTE_TYPE}.
* If not set within the YAML, this will be null.
*/
@JsonProperty("member-attribute-type")
private String memberAttributeType;
/**
* The default configuration options for all parameters.
*/
private LDAPConfiguration defaultConfig = new DefaultLDAPConfiguration();
/**
* Supplier of default values for LDAP configurations. Unlike
* {@link java.util.function.Supplier}, the {@link #get()} function of
* DefaultSupplier may throw a {@link GuacamoleException}.
*
* @param <T>
* The type of value returned by this DefaultSupplier.
*/
@FunctionalInterface
private interface DefaultSupplier<T> {
/**
* Returns the value supplied by this DefaultSupplier. The value
* returned is not cached and may be non-deterministic.
*
* @return
* The value supplied by this DefaultSupplier.
*
* @throws GuacamoleException
* If an error occurs while producing/retrieving the value.
*/
T get() throws GuacamoleException;
}
/**
* Returns the given value, if non-null. If null, the given default value
* is returned.
*
* @param <T>
* The type of value accepted and returned.
*
* @param value
* The possibly null value to return if non-null.
*
* @param defaultValue
* A function which supplies the value to return if the provided value
* is null.
*
* @return
* The provided value, if non-null, otherwise the provided default
* value.
*
* @throws GuacamoleException
* If an error occurs while producing/retrieving the default value.
*/
private <T> T withDefault(T value, DefaultSupplier<T> defaultValue)
throws GuacamoleException {
return value != null ? value : defaultValue.get();
}
/**
* Parses and returns the given value, if non-null. If null, the given
* default value is returned.
*
* @param <T>
* The type of value accepted and returned.
*
* @param property
* The GuacamoleProperty implementation to use to parse the provided
* String value.
*
* @param value
* The possibly null value to return if non-null.
*
* @param defaultValue
* A function which supplies the value to return if the provided value
* is null.
*
* @return
* The provided value, if non-null, otherwise the provided default
* value.
*
* @throws GuacamoleException
* If an error occurs while producing/retrieving the default value.
*/
private <T> T withDefault(GuacamoleProperty<T> property, String value,
DefaultSupplier<T> defaultValue)
throws GuacamoleException {
return withDefault(property.parseValue(value), defaultValue);
}
/**
* Sets the LDAPConfiguration that should be used for the default values of
* any configuration options omitted from the YAML. If not set, an instance
* of {@link DefaultLDAPConfiguration} will be used.
*
* @param defaultConfig
* The LDAPConfiguration to use for the default values of any omitted
* configuration options.
*/
public void setDefaults(LDAPConfiguration defaultConfig) {
this.defaultConfig = defaultConfig;
}
@Override
public String appliesTo(String username) throws GuacamoleException {
// Match any user by default
if (matchUsernames == null || matchUsernames.isEmpty())
return username;
for (Pattern pattern : matchUsernames) {
Matcher matcher = pattern.matcher(username);
if (matcher.matches())
return matcher.groupCount() >= 1 ? matcher.group(1) : username;
}
return null;
}
@Override
public String getServerHostname() throws GuacamoleException {
return withDefault(hostname, defaultConfig::getServerHostname);
}
@Override
public int getServerPort() throws GuacamoleException {
return withDefault(port, () -> getEncryptionMethod().DEFAULT_PORT);
}
@Override
public Collection<String> getUsernameAttributes() throws GuacamoleException {
return withDefault(usernameAttributes, defaultConfig::getUsernameAttributes);
}
@Override
public Dn getUserBaseDN() throws GuacamoleException {
return withDefault(LDAPGuacamoleProperties.LDAP_USER_BASE_DN,
userBaseDn, defaultConfig::getUserBaseDN);
}
@Override
public Dn getConfigurationBaseDN() throws GuacamoleException {
return withDefault(LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN,
configBaseDn, defaultConfig::getConfigurationBaseDN);
}
@Override
public Collection<String> getGroupNameAttributes() throws GuacamoleException {
return withDefault(groupNameAttributes, defaultConfig::getGroupNameAttributes);
}
@Override
public Dn getGroupBaseDN() throws GuacamoleException {
return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN,
groupBaseDn, defaultConfig::getGroupBaseDN);
}
@Override
public String getSearchBindDN() throws GuacamoleException {
return withDefault(searchBindDn, defaultConfig::getSearchBindDN);
}
@Override
public String getSearchBindPassword() throws GuacamoleException {
return withDefault(searchBindPassword, defaultConfig::getSearchBindPassword);
}
@Override
public EncryptionMethod getEncryptionMethod() throws GuacamoleException {
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 {
return withDefault(maxSearchResults, defaultConfig::getMaxResults);
}
@Override
public AliasDerefMode getDereferenceAliases() throws GuacamoleException {
return withDefault(LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES,
dereferenceAliases, defaultConfig::getDereferenceAliases);
}
@Override
public boolean getFollowReferrals() throws GuacamoleException {
return withDefault(followReferrals, defaultConfig::getFollowReferrals);
}
@Override
public int getMaxReferralHops() throws GuacamoleException {
return withDefault(maxReferralHops, defaultConfig::getMaxReferralHops);
}
@Override
public ExprNode getUserSearchFilter() throws GuacamoleException {
return withDefault(LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER,
userSearchFilter, defaultConfig::getUserSearchFilter);
}
@Override
public ExprNode getGroupSearchFilter() throws GuacamoleException {
return withDefault(LDAPGuacamoleProperties.LDAP_GROUP_SEARCH_FILTER,
groupSearchFilter, defaultConfig::getGroupSearchFilter);
}
@Override
public int getOperationTimeout() throws GuacamoleException {
return withDefault(operationTimeout, defaultConfig::getOperationTimeout);
}
@Override
public int getNetworkTimeout() throws GuacamoleException {
return withDefault(networkTimeout, defaultConfig::getNetworkTimeout);
}
@Override
public Collection<String> getAttributes() throws GuacamoleException {
return withDefault(userAttributes, defaultConfig::getAttributes);
}
@Override
public String getMemberAttribute() throws GuacamoleException {
return withDefault(memberAttribute, defaultConfig::getMemberAttribute);
}
@Override
public MemberAttributeType getMemberAttributeType() throws GuacamoleException {
return withDefault(LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE_TYPE,
memberAttributeType, defaultConfig::getMemberAttributeType);
}
}

View File

@@ -0,0 +1,337 @@
/*
* 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 java.util.Collection;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.GuacamoleException;
/**
* Configuration information defining how a particular LDAP server should be
* queried.
*/
public interface LDAPConfiguration {
/**
* Tests whether this LDAPConfiguration applies to the user having the
* given username. If the configuration applies, the username that should
* be used to derive the user's DN is returned.
*
* @param username
* The username to test.
*
* @return
* The username that should be used to derive this user's DN, or null
* if the configuration does not apply.
*
* @throws GuacamoleException
* If an error prevents testing against this configuration.
*/
String appliesTo(String username) throws GuacamoleException;
/**
* Returns the hostname or IP address of the LDAP server.
*
* @return
* The hostname or IP address of the LDAP server.
*
* @throws GuacamoleException
* If the hostname or IP address of the LDAP server cannot be
* retrieved.
*/
String getServerHostname() throws GuacamoleException;
/**
* Returns the port of the LDAP server. The default value depends on which
* encryption method is being used. For unencrypted LDAP and STARTTLS, this
* will be 389. For LDAPS (LDAP over SSL) this will be 636.
*
* @return
* The port of the LDAP server.
*
* @throws GuacamoleException
* If the port of the LDAP server cannot be retrieved.
*/
int getServerPort() throws GuacamoleException;
/**
* Returns all username attributes which should be used to query and bind
* users using the LDAP directory.
*
* @return
* The username attributes which should be used to query and bind users
* using the LDAP directory.
*
* @throws GuacamoleException
* If the username attributes cannot be retrieved.
*/
Collection<String> getUsernameAttributes() throws GuacamoleException;
/**
* Returns the base DN under which all Guacamole users will be stored
* within the LDAP directory.
*
* @return
* The base DN under which all Guacamole users will be stored within
* the LDAP directory.
*
* @throws GuacamoleException
* If the user base DN cannot be retrieved.
*/
Dn getUserBaseDN() throws GuacamoleException;
/**
* Returns the base DN under which all Guacamole configurations
* (connections) will be stored within the LDAP directory. If Guacamole
* configurations will not be stored within LDAP, null is returned.
*
* @return
* The base DN under which all Guacamole configurations will be stored
* within the LDAP directory, or null if no Guacamole configurations
* will be stored within the LDAP directory.
*
* @throws GuacamoleException
* If the configuration base DN cannot be retrieved.
*/
Dn getConfigurationBaseDN() throws GuacamoleException;
/**
* Returns all attributes which should be used to determine the unique
* identifier of each user group.
*
* @return
* The attributes which should be used to determine the unique
* identifier of each group.
*
* @throws GuacamoleException
* If the group name attributes cannot be retrieved.
*/
Collection<String> getGroupNameAttributes() throws GuacamoleException;
/**
* Returns the base DN under which all Guacamole role based access control
* (RBAC) groups will be stored within the LDAP directory. If RBAC will not
* be used, null is returned.
*
* @return
* The base DN under which all Guacamole RBAC groups will be stored
* within the LDAP directory, or null if RBAC will not be used.
*
* @throws GuacamoleException
* If the group base DN cannot be retrieved.
*/
Dn getGroupBaseDN() throws GuacamoleException;
/**
* Returns the login that should be used when searching for the DNs of users
* attempting to authenticate. If no such search should be performed, null
* is returned.
*
* @return
* The DN that should be used when searching for the DNs of users
* attempting to authenticate, or null if no such search should be
* performed.
*
* @throws GuacamoleException
* If the search bind DN cannot be retrieved.
*/
String getSearchBindDN() throws GuacamoleException;
/**
* Returns the password that should be used when binding to the LDAP server
* using the DN returned by getSearchBindDN(). If no password should be
* used, null is returned.
*
* @return
* The password that should be used when binding to the LDAP server
* using the DN returned by getSearchBindDN(), or null if no password
* should be used.
*
* @throws GuacamoleException
* If the search bind password cannot be retrieved.
*/
String getSearchBindPassword() throws GuacamoleException;
/**
* Returns the encryption method that should be used when connecting to the
* LDAP server.
*
* @return
* The encryption method that should be used when connecting to the
* LDAP server.
*
* @throws GuacamoleException
* 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.
*
* @return
* The maximum number of results a LDAP query can return.
*
* @throws GuacamoleException
* If the maximum number of results cannot be retrieved.
*/
int getMaxResults() throws GuacamoleException;
/**
* Returns whether or not LDAP aliases will be dereferenced.
*
* @return
* The LDAP alias dereferencing mode.
*
* @throws GuacamoleException
* If the LDAP alias dereferencing mode cannot be retrieved.
*/
AliasDerefMode getDereferenceAliases() throws GuacamoleException;
/**
* Returns whether referrals should be automatically followed.
*
* @return
* Whether referrals should be followed.
*
* @throws GuacamoleException
* If the configuration information determining whether LDAP referrals
* should be followed cannot be retrieved.
*/
boolean getFollowReferrals() throws GuacamoleException;
/**
* Returns the maximum number of referral hops to follow.
*
* @return
* The maximum number of referral hops to follow.
*
* @throws GuacamoleException
* If the maximum number of referral hops cannot be retrieved.
*/
int getMaxReferralHops() throws GuacamoleException;
/**
* Returns the search filter that should be used when querying the
* LDAP server for Guacamole users.
*
* @return
* The search filter that should be used when querying the
* LDAP server for users that are valid in Guacamole.
*
* @throws GuacamoleException
* If the user search filter cannot be retrieved.
*/
ExprNode getUserSearchFilter() throws GuacamoleException;
/**
* Returns the search filter that should be used when querying the
* LDAP server for Guacamole groups.
*
* @return
* The search filter that should be used when querying the
* LDAP server for groups that are valid in Guacamole.
*
* @throws GuacamoleException
* If the group search filter cannot be retrieved.
*/
ExprNode getGroupSearchFilter() throws GuacamoleException;
/**
* Returns the maximum number of milliseconds to wait for a response when
* communicating with the LDAP server.
*
* @return
* The maximum number of milliseconds to wait for responses from the
* LDAP server.
*
* @throws GuacamoleException
* If the LDAP network timeout cannot be retrieved.
*/
int getNetworkTimeout() throws GuacamoleException;
/**
* Returns the maximum number of seconds to wait for LDAP operations.
*
* @return
* The maximum number of seconds to wait for LDAP operations.
*
* @throws GuacamoleException
* If the LDAP operation timeout cannot be retrieved.
*/
int getOperationTimeout() throws GuacamoleException;
/**
* Returns names of any LDAP user attributes that should be made available
* as parameter tokens.
*
* @return
* A list of all LDAP user attributes that should be made available as
* parameter tokens.
*
* @throws GuacamoleException
* If the names of the LDAP user attributes to be exposed as parameter
* tokens cannot be retrieved.
*/
Collection<String> getAttributes() throws GuacamoleException;
/**
* Returns the name of the LDAP attribute used to enumerate members in a
* group.
*
* @return
* The name of the LDAP attribute to use to enumerate
* members in a group.
*
* @throws GuacamoleException
* If the group member attribute cannot be retrieved.
*/
String getMemberAttribute() throws GuacamoleException;
/**
* Returns whether the LDAP attribute used to enumerate members in a group
* specifies a UID or DN.
*
* @return
* The type of data contained in the LDAP attribute used to enumerate
* members in a group.
*
* @throws GuacamoleException
* If the type of attribute used to enumerate group members cannot be
* retrieved.
*/
MemberAttributeType getMemberAttributeType() throws GuacamoleException;
}

View File

@@ -0,0 +1,310 @@
/*
* 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.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
import org.apache.guacamole.properties.EnumGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
/**
* Provides properties required for use of the LDAP authentication provider.
* These properties will be read from guacamole.properties when the LDAP
* authentication provider is used.
*/
public class LDAPGuacamoleProperties {
/**
* This class should not be instantiated.
*/
private LDAPGuacamoleProperties() {}
/**
* The base DN to search for Guacamole configurations.
*/
public static final LdapDnGuacamoleProperty LDAP_CONFIG_BASE_DN =
new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-config-base-dn"; }
};
/**
* The base DN of users. All users must be contained somewhere within the
* subtree of this DN. If the LDAP authentication will not be given its own
* credentials for querying other LDAP users, all users must be direct
* children of this base DN, varying only by LDAP_USERNAME_ATTRIBUTE.
*/
public static final LdapDnGuacamoleProperty LDAP_USER_BASE_DN =
new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-user-base-dn"; }
};
/**
* The base DN of role based access control (RBAC) groups. All groups which
* will be used for RBAC must be contained somewhere within the subtree of
* this DN.
*/
public static final LdapDnGuacamoleProperty LDAP_GROUP_BASE_DN =
new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-group-base-dn"; }
};
/**
* The attribute or attributes which identify users. One of these
* attributes must be present within each Guacamole user's record in the
* LDAP directory. If the LDAP authentication will not be given its own
* credentials for querying other LDAP users, this list may contain only
* one attribute, and the concatenation of that attribute and the value of
* LDAP_USER_BASE_DN must equal the user's full DN.
*/
public static final StringGuacamoleProperty LDAP_USERNAME_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-username-attribute"; }
};
/**
* The attribute or attributes which identify user groups. One of these
* attributes must be present within each Guacamole user group's record in
* the LDAP directory for that group to be visible.
*/
public static final StringGuacamoleProperty LDAP_GROUP_NAME_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-group-name-attribute"; }
};
/**
* The port on the LDAP server to connect to when authenticating users.
*/
public static final IntegerGuacamoleProperty LDAP_PORT =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-port"; }
};
/**
* The hostname of the LDAP server to connect to when authenticating users.
*/
public static final StringGuacamoleProperty LDAP_HOSTNAME =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-hostname"; }
};
/**
* The user that the LDAP extension should bind as when searching for the
* accounts of users attempting to log in. The format of this parameter
* will vary based on the LDAP server implementation - often it is expected
* to be in full LDAP DN format; however various LDAP server implementations
* allow this to be in other formats (e.g. Active Directory allows
* User Principal Name, or UPN, format). For this reason the configuration
* allows this to be any string.
*/
public static final StringGuacamoleProperty LDAP_SEARCH_BIND_DN =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-search-bind-dn"; }
};
/**
* The password to provide to the LDAP server when binding as
* LDAP_SEARCH_BIND_DN. If LDAP_SEARCH_BIND_DN is not specified, this
* property has no effect. If this property is not specified, no password
* will be provided when attempting to bind as LDAP_SEARCH_BIND_DN.
*/
public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-search-bind-password"; }
};
/**
* The encryption method to use when connecting to the LDAP server, if any.
* The chosen method will also dictate the default port if not already
* explicitly specified via LDAP_PORT.
*/
public static final EnumGuacamoleProperty<EncryptionMethod> LDAP_ENCRYPTION_METHOD =
new EnumGuacamoleProperty<EncryptionMethod>(EncryptionMethod.class) {
@Override
public String getName() { return "ldap-encryption-method"; }
};
public static final EnumGuacamoleProperty<LDAPSSLProtocol> LDAP_SSL_PROTOCOL =
new EnumGuacamoleProperty<LDAPSSLProtocol>(LDAPSSLProtocol.class) {
@Override
public String getName() { return "ldap-ssl-protocol"; }
};
/**
* The maximum number of results a LDAP query can return.
*/
public static final IntegerGuacamoleProperty LDAP_MAX_SEARCH_RESULTS =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-max-search-results"; }
};
/**
* Property that controls whether or not the LDAP connection follows
* (dereferences) aliases as it searches the tree.
*/
public static final EnumGuacamoleProperty<AliasDerefMode> LDAP_DEREFERENCE_ALIASES =
new EnumGuacamoleProperty<AliasDerefMode>(
"never", AliasDerefMode.NEVER_DEREF_ALIASES,
"searching", AliasDerefMode.DEREF_IN_SEARCHING,
"finding", AliasDerefMode.DEREF_FINDING_BASE_OBJ,
"always", AliasDerefMode.DEREF_ALWAYS
) {
@Override
public String getName() { return "ldap-dereference-aliases"; }
};
/**
* A search filter to apply to user LDAP queries.
*/
public static final LdapFilterGuacamoleProperty LDAP_USER_SEARCH_FILTER =
new LdapFilterGuacamoleProperty() {
@Override
public String getName() { return "ldap-user-search-filter"; }
};
/**
* A search filter to apply to group LDAP queries.
*/
public static final LdapFilterGuacamoleProperty LDAP_GROUP_SEARCH_FILTER =
new LdapFilterGuacamoleProperty() {
@Override
public String getName() { return "ldap-group-search-filter"; }
};
/**
* Whether or not we should follow referrals.
*/
public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "ldap-follow-referrals"; }
};
/**
* Maximum number of referral hops to follow.
*/
public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-max-referral-hops"; }
};
/**
* Number of seconds to wait for LDAP operations to complete.
*/
public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-operation-timeout"; }
};
/**
* Number of milliseconds to wait for responses from the LDAP server.
*/
public static final IntegerGuacamoleProperty LDAP_NETWORK_TIMEOUT =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-network-timeout"; }
};
/**
* Custom attribute or attributes to query from Guacamole user's record in
* the LDAP directory.
*/
public static final StringGuacamoleProperty LDAP_USER_ATTRIBUTES =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-user-attributes"; }
};
/**
* LDAP attribute used to enumerate members of a group in the LDAP directory.
*/
public static final StringGuacamoleProperty LDAP_MEMBER_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-member-attribute"; }
};
/**
* Specify the type of data contained in 'ldap-member-attribute'.
*/
public static final EnumGuacamoleProperty<MemberAttributeType> LDAP_MEMBER_ATTRIBUTE_TYPE =
new EnumGuacamoleProperty<MemberAttributeType>(MemberAttributeType.class) {
@Override
public String getName() { return "ldap-member-attribute-type"; }
};
}

View File

@@ -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;
}
}

View File

@@ -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.ldap.conf;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty that converts a string to a Dn that can be used
* in LDAP connections. An exception is thrown if the provided DN is invalid
* and cannot be parsed.
*/
public abstract class LdapDnGuacamoleProperty implements GuacamoleProperty<Dn> {
@Override
public Dn parseValue(String value) throws GuacamoleException {
if (value == null)
return null;
try {
return new Dn(value);
}
catch (LdapInvalidDnException e) {
throw new GuacamoleServerException("The DN \"" + value + "\" is invalid.", e);
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 java.text.ParseException;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.FilterParser;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty with a value of an ExprNode query filter. The string
* provided is passed through the FilterParser returning the ExprNode object,
* or an exception is thrown if the filter is invalid and cannot be correctly
* parsed.
*/
public abstract class LdapFilterGuacamoleProperty implements GuacamoleProperty<ExprNode> {
@Override
public ExprNode parseValue(String value) throws GuacamoleException {
// No value provided, so return null.
if (value == null)
return null;
try {
return FilterParser.parse(value);
}
catch (ParseException e) {
throw new GuacamoleServerException("\"" + value + "\" is not a valid LDAP filter.", e);
}
}
}

View File

@@ -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.auth.ldap.conf;
import org.apache.guacamole.properties.EnumGuacamoleProperty.PropertyValue;
/**
* All possible means of describing membership within LDAP group directory
* records.
*/
public enum MemberAttributeType {
/**
* Group membership is specified by DN.
*/
@PropertyValue("dn")
DN,
/**
* Group membership is specified by usercode.
*/
@PropertyValue("uid")
UID;
}

View File

@@ -0,0 +1,403 @@
/*
* 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.connection;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.filter.AndNode;
import org.apache.directory.api.ldap.model.filter.EqualityNode;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.OrNode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
import org.apache.guacamole.auth.ldap.group.UserGroupService;
import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration;
import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration.EncryptionMethod;
import org.apache.guacamole.net.auth.TokenInjectingConnection;
import org.apache.guacamole.net.auth.simple.SimpleConnection;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for querying the connections available to a particular Guacamole
* user according to an LDAP directory.
*/
public class ConnectionService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
/**
* The name of the LDAP attribute that stores connection configuration
* parameters for Guacamole.
*/
public static final String LDAP_ATTRIBUTE_PARAMETER = "guacConfigParameter";
/**
* The name of the LDAP attribute that stores the protocol for a Guacamole
* connection.
*/
public static final String LDAP_ATTRIBUTE_PROTOCOL = "guacConfigProtocol";
/**
* The name of the LDAP attribute that stores guacd proxy hostname.
*/
public static final String LDAP_ATTRIBUTE_PROXY_HOSTNAME = "guacConfigProxyHostname";
/**
* The name of the LDAP attribute that stores guacd proxy port.
*/
public static final String LDAP_ATTRIBUTE_PROXY_PORT = "guacConfigProxyPort";
/**
* The name of the LDAP attribute that stores guacd proxy encryption method.
*/
public static final String LDAP_ATTRIBUTE_PROXY_ENCRYPTION = "guacConfigProxyEncryption";
/**
* Service for executing LDAP queries.
*/
@Inject
private ObjectQueryService queryService;
/**
* Service for retrieving user groups.
*/
@Inject
private UserGroupService userGroupService;
/**
* The objectClass that is present on any Guacamole connections stored
* in LDAP.
*/
public static final String CONNECTION_LDAP_OBJECT_CLASS = "guacConfigGroup";
/**
* The attribute name that uniquely identifies a Guacamole connection object
* in LDAP.
*/
public static final String LDAP_ATTRIBUTE_NAME_ID = "cn";
/**
* The LDAP attribute name where the Guacamole connection protocol is stored.
*/
public static final String LDAP_ATTRIBUTE_NAME_PROTOCOL = "guacConfigProtocol";
/**
* The LDAP attribute name that contains any connection parameters.
*/
public static final String LDAP_ATTRIBUTE_NAME_PARAMETER = "guacConfigParameter";
/**
* The LDAP attribute name that provides group-based access control for
* Guacamole connection objects.
*/
public static final String LDAP_ATTRIBUTE_NAME_GROUPS = "seeAlso";
/**
* A list of all attribute names that could be associated with a Guacamole
* connection object in LDAP.
*/
public static final Collection<String> GUAC_CONFIG_LDAP_ATTRIBUTES =
Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
LDAP_ATTRIBUTE_NAME_ID,
LDAP_ATTRIBUTE_NAME_PROTOCOL,
LDAP_ATTRIBUTE_NAME_PARAMETER,
LDAP_ATTRIBUTE_NAME_GROUPS
)));
/**
* Returns all Guacamole connections accessible to the given user.
*
* @param user
* The AuthenticatedUser object associated with the user who is
* currently authenticated with Guacamole.
*
* @return
* All connections accessible to the user currently bound under the
* given LDAP connection, as a map of connection identifier to
* corresponding connection object.
*
* @throws GuacamoleException
* If an error occurs preventing retrieval of connections.
*/
public Map<String, Connection> getConnections(LDAPAuthenticatedUser user)
throws GuacamoleException {
ConnectedLDAPConfiguration ldapConfig = user.getLDAPConfiguration();
// Do not return any connections if base DN is not specified
Dn configurationBaseDN = ldapConfig.getConfigurationBaseDN();
if (configurationBaseDN == null)
return Collections.<String, Connection>emptyMap();
try {
// Get the search filter for finding connections accessible by the
// current user
ExprNode connectionSearchFilter = getConnectionSearchFilter(user);
// Find all Guacamole connections for the given user by
// looking for direct membership in the guacConfigGroup
// and possibly any groups the user is a member of that are
// referred to in the seeAlso attribute of the guacConfigGroup.
List<Entry> results = queryService.search(ldapConfig, ldapConfig.getLDAPConnection(),
configurationBaseDN, connectionSearchFilter, 0, GUAC_CONFIG_LDAP_ATTRIBUTES);
// Return a map of all readable connections
return queryService.asMap(results, (entry) -> {
// Get common name (CN)
Attribute cn = entry.get(LDAP_ATTRIBUTE_NAME_ID);
if (cn == null) {
logger.warn("{} is missing a {}.",
CONNECTION_LDAP_OBJECT_CLASS, LDAP_ATTRIBUTE_NAME_ID);
return null;
}
String cnName;
try {
cnName = cn.getString();
}
catch (LdapInvalidAttributeValueException e) {
logger.error("Invalid value for {} attribute: {}",
LDAP_ATTRIBUTE_NAME_ID, e.getMessage());
logger.debug("LDAP exception while getting CN attribute.", e);
return null;
}
// Get associated protocol
Attribute protocol = entry.get(LDAP_ATTRIBUTE_NAME_PROTOCOL);
if (protocol == null) {
logger.warn("{} \"{}\" is missing the "
+ "required \"{}\" attribute.",
CONNECTION_LDAP_OBJECT_CLASS,
cnName, LDAP_ATTRIBUTE_NAME_PROTOCOL);
return null;
}
// Set protocol
GuacamoleConfiguration config = new GuacamoleConfiguration();
try {
config.setProtocol(protocol.getString());
}
catch (LdapInvalidAttributeValueException e) {
logger.error("Invalid value of the protocol entry: {}", e.getMessage());
logger.debug("LDAP exception when getting protocol value.", e);
return null;
}
// Get proxy configuration, if any
GuacamoleProxyConfiguration proxyConfig;
try {
proxyConfig = getProxyConfiguration(entry);
}
catch (GuacamoleException e) {
logger.error("Failed to retrieve proxy configuration.", e.getMessage());
logger.debug("Guacamole Exception when retrieving proxy configuration.", e);
return null;
}
// Get parameters, if any
Attribute parameterAttribute = entry.get(LDAP_ATTRIBUTE_NAME_PARAMETER);
if (parameterAttribute != null) {
// For each parameter
while (parameterAttribute.size() > 0) {
String parameter;
try {
parameter = parameterAttribute.getString();
}
catch (LdapInvalidAttributeValueException e) {
logger.warn("Parameter value not valid for {}: {}", cnName, e.getMessage());
logger.debug("LDAP exception when getting parameter value.", e);
return null;
}
parameterAttribute.remove(parameter);
// Parse parameter
int equals = parameter.indexOf('=');
if (equals != -1) {
// Parse name
String name = parameter.substring(0, equals);
String value = parameter.substring(equals+1);
config.setParameter(name, value);
}
}
}
// Store connection using cn for both identifier and name
Connection connection = new SimpleConnection(cnName, cnName, proxyConfig, config, true);
connection.setParentIdentifier(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP);
// Inject LDAP-specific tokens only if LDAP handled user
// authentication
if (user instanceof LDAPAuthenticatedUser)
connection = new TokenInjectingConnection(connection, user.getTokens());
return connection;
});
}
catch (LdapException e) {
throw new GuacamoleServerException("Error while querying for connections.", e);
}
}
/**
* Returns an LDAP search filter which queries all connections accessible
* by the given user.
*
* @param user
* The AuthenticatedUser object associated with the user who is
* currently authenticated with Guacamole.
*
* @return
* An LDAP search filter which queries all guacConfigGroup objects
* accessible by the user having the given DN.
*
* @throws LdapException
* If an error occurs preventing retrieval of user groups.
*
* @throws GuacamoleException
* If an error occurs retrieving the group base DN.
*/
private ExprNode getConnectionSearchFilter(LDAPAuthenticatedUser user)
throws LdapException, GuacamoleException {
ConnectedLDAPConfiguration config = user.getLDAPConfiguration();
Dn userDN = config.getBindDN();
AndNode searchFilter = new AndNode();
// Add the prefix to the search filter, prefix filter searches for guacConfigGroups with the userDN as the member attribute value
searchFilter.addNode(new EqualityNode("objectClass", CONNECTION_LDAP_OBJECT_CLASS));
// Apply group filters
OrNode groupFilter = new OrNode();
groupFilter.addNode(new EqualityNode(config.getMemberAttribute(),
userDN.toString()));
// Additionally filter by group membership if the current user is a
// member of any user groups
List<Entry> userGroups = userGroupService.getParentUserGroupEntries(config, userDN);
if (!userGroups.isEmpty()) {
userGroups.forEach(entry ->
groupFilter.addNode(new EqualityNode(LDAP_ATTRIBUTE_NAME_GROUPS,entry.getDn().toString()))
);
}
// Complete the search filter.
searchFilter.addNode(groupFilter);
return searchFilter;
}
/**
* Given an LDAP entry that stores a GuacamoleConfiguration, generate a
* GuacamoleProxyConfiguration that tells the client how to connect to guacd.
* If the proxy configuration values are not found in the LDAP entry the
* defaults from the environment are used. If errors occur while trying to
* ready or parse values from the LDAP entry a GuacamoleException is thrown.
*
* @param connectionEntry
* The LDAP entry that should be checked for proxy configuration values.
*
* @return
* The GuacamoleProxyConfiguration that contains information on how
* to contact guacd for the given Guacamole connection configuration.
*
* @throws GuacamoleException
* If errors occur trying to parse LDAP values from the entry.
*/
private GuacamoleProxyConfiguration getProxyConfiguration(Entry connectionEntry)
throws GuacamoleException {
try {
// Get default proxy configuration values
GuacamoleProxyConfiguration proxyConfig = LocalEnvironment.getInstance().getDefaultGuacamoleProxyConfiguration();
String proxyHostname = proxyConfig.getHostname();
int proxyPort = proxyConfig.getPort();
EncryptionMethod proxyEncryption = proxyConfig.getEncryptionMethod();
// Get the proxy hostname
Attribute proxyHostAttr = connectionEntry.get(LDAP_ATTRIBUTE_PROXY_HOSTNAME);
if (proxyHostAttr != null && proxyHostAttr.size() > 0)
proxyHostname = proxyHostAttr.getString();
// Get the proxy port
Attribute proxyPortAttr = connectionEntry.get(LDAP_ATTRIBUTE_PROXY_PORT);
if (proxyPortAttr != null && proxyPortAttr.size() > 0)
proxyPort = Integer.parseInt(proxyPortAttr.getString());
// Get the proxy encryption method
Attribute proxyEncryptionAttr = connectionEntry.get(LDAP_ATTRIBUTE_PROXY_ENCRYPTION);
if (proxyEncryptionAttr != null && proxyEncryptionAttr.size() > 0) {
try {
proxyEncryption = EncryptionMethod.valueOf(proxyEncryptionAttr.getString());
}
catch (IllegalArgumentException e) {
throw new GuacamoleServerException("Unknown encryption method specified, value must be either \"NONE\" or \"SSL\".", e);
}
}
// Return a new proxy configuration
return new GuacamoleProxyConfiguration(proxyHostname, proxyPort, proxyEncryption);
}
catch (LdapInvalidAttributeValueException e) {
logger.error("Invalid value in proxy configuration: {}", e.getMessage());
logger.debug("LDAP exception fetching proxy attribute value.", e);
throw new GuacamoleServerException("Invalid LDAP value in proxy configuration.", e);
}
}
}

View File

@@ -0,0 +1,293 @@
/*
* 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.group;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.filter.AndNode;
import org.apache.directory.api.ldap.model.filter.EqualityNode;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.NotNode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.auth.ldap.conf.MemberAttributeType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration;
import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
import org.apache.guacamole.net.auth.UserGroup;
import org.apache.guacamole.net.auth.simple.SimpleUserGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for querying user group membership and retrieving user groups
* visible to a particular Guacamole user.
*/
public class UserGroupService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(UserGroupService.class);
/**
* Service for executing LDAP queries.
*/
@Inject
private ObjectQueryService queryService;
/**
* Returns the base search filter which should be used to retrieve user
* groups which do not represent Guacamole connections. As excluding the
* guacConfigGroup object class may not work as expected if it is not
* defined (may always return zero results), it should only be explicitly
* excluded if it is expected to have been defined.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @return
* The base search filter which should be used to retrieve user groups.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private ExprNode getGroupSearchFilter(LDAPConfiguration config) throws GuacamoleException {
// Use filter defined by "ldap-group-search-filter" as basis for all
// retrieval of user groups
ExprNode groupFilter = config.getGroupSearchFilter();
// Explicitly exclude guacConfigGroup object class only if it should
// be assumed to be defined (query may fail due to no such object
// class existing otherwise)
if (config.getConfigurationBaseDN() != null) {
groupFilter = new AndNode(
groupFilter,
new NotNode(new EqualityNode<String>("objectClass", "guacConfigGroup"))
);
}
return groupFilter;
}
/**
* Returns all Guacamole user groups accessible to the given user.
*
* @param user
* The AuthenticatedUser object associated with the user who is
* currently authenticated with Guacamole.
*
* @return
* All user groups accessible to the user currently bound under the
* given LDAP connection, as a map of user group identifier to
* corresponding UserGroup object.
*
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
public Map<String, UserGroup> getUserGroups(LDAPAuthenticatedUser user)
throws GuacamoleException {
ConnectedLDAPConfiguration config = user.getLDAPConfiguration();
// Do not return any user groups if base DN is not specified
Dn groupBaseDN = config.getGroupBaseDN();
if (groupBaseDN == null)
return Collections.emptyMap();
// Gather all attributes relevant for a group
String memberAttribute = config.getMemberAttribute();
Collection<String> groupAttributes = new HashSet<>(config.getGroupNameAttributes());
groupAttributes.add(memberAttribute);
// Retrieve all visible user groups which are not guacConfigGroups
Collection<String> attributes = config.getGroupNameAttributes();
List<Entry> results = queryService.search(
config,
config.getLDAPConnection(),
groupBaseDN,
getGroupSearchFilter(config),
attributes,
null,
groupAttributes
);
// Convert retrieved user groups to map of identifier to Guacamole
// user group object
return queryService.asMap(results, entry -> {
// Translate entry into UserGroup object having proper identifier
try {
String name = queryService.getIdentifier(entry, attributes);
if (name != null)
return new SimpleUserGroup(name);
}
catch (LdapInvalidAttributeValueException e) {
return null;
}
// Ignore user groups which lack a name attribute
logger.debug("User group \"{}\" is missing a name attribute "
+ "and will be ignored.", entry.getDn().toString());
return null;
});
}
/**
* Returns the LDAP entries representing all user groups that the given
* user is a member of. Only user groups which are readable by the current
* user will be retrieved.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param userDN
* The DN of the user whose group membership should be retrieved.
*
* @return
* The LDAP entries representing all readable parent user groups of the
* user having the given DN.
*
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
public List<Entry> getParentUserGroupEntries(ConnectedLDAPConfiguration config, Dn userDN)
throws GuacamoleException {
// Do not return any user groups if base DN is not specified
Dn groupBaseDN = config.getGroupBaseDN();
if (groupBaseDN == null)
return Collections.emptyList();
// memberAttribute specified in properties could contain DN or username
MemberAttributeType memberAttributeType = config.getMemberAttributeType();
String userIDorDN = userDN.toString();
Collection<String> userAttributes = config.getUsernameAttributes();
if (memberAttributeType == MemberAttributeType.UID) {
// Retrieve user objects with userDN
List<Entry> userEntries = queryService.search(
config,
config.getLDAPConnection(),
userDN,
config.getUserSearchFilter(),
0,
userAttributes);
// ... there can surely only be one
if (userEntries.size() != 1)
logger.warn("user DN \"{}\" does not return unique value "
+ "and will be ignored", userDN.toString());
else {
// determine unique identifier for user
Entry userEntry = userEntries.get(0);
try {
userIDorDN = queryService.getIdentifier(userEntry,
userAttributes);
}
catch (LdapInvalidAttributeValueException e) {
logger.error("User group missing identifier: {}",
e.getMessage());
logger.debug("LDAP exception while getting "
+ "group identifier.", e);
}
}
}
// Gather all attributes relevant for a group
String memberAttribute = config.getMemberAttribute();
Collection<String> groupAttributes = new HashSet<>(config.getGroupNameAttributes());
groupAttributes.add(memberAttribute);
// Get all groups the user is a member of starting at the groupBaseDN,
// excluding guacConfigGroups
return queryService.search(
config,
config.getLDAPConnection(),
groupBaseDN,
getGroupSearchFilter(config),
Collections.singleton(memberAttribute),
userIDorDN,
groupAttributes
);
}
/**
* Returns the identifiers of all user groups that the given user is a
* member of. Only identifiers of user groups which are readable by the
* current user will be retrieved.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param userDN
* The DN of the user whose group membership should be retrieved.
*
* @return
* The identifiers of all readable parent user groups of the user
* having the given DN.
*
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
public Set<String> getParentUserGroupIdentifiers(ConnectedLDAPConfiguration config, Dn userDN)
throws GuacamoleException {
Collection<String> attributes = config.getGroupNameAttributes();
List<Entry> userGroups = getParentUserGroupEntries(config, userDN);
Set<String> identifiers = new HashSet<>(userGroups.size());
userGroups.forEach(entry -> {
// Determine unique identifier for user group
try {
String name = queryService.getIdentifier(entry, attributes);
if (name != null)
identifiers.add(name);
// Ignore user groups which lack a name attribute
else
logger.debug("User group \"{}\" is missing a name attribute "
+ "and will be ignored.", entry.getDn().toString());
}
catch (LdapInvalidAttributeValueException e) {
logger.error("User group missing identifier: {}",
e.getMessage());
logger.debug("LDAP exception while getting group identifier.", e);
}
});
return identifiers;
}
}

View File

@@ -0,0 +1,159 @@
/*
* 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.user;
import com.google.inject.Inject;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration;
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
/**
* An LDAP-specific implementation of AuthenticatedUser, associating a
* particular set of credentials with the LDAP authentication provider.
*/
public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser {
/**
* Reference to the authentication provider associated with this
* authenticated user.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* The credentials provided when this user was authenticated.
*/
private Credentials credentials;
/**
* Name/value pairs to be applied as parameter tokens when connections
* are established using this AuthenticatedUser.
*/
private Map<String, String> tokens;
/**
* The unique identifiers of all user groups which affect the permissions
* available to this user.
*/
private Set<String> effectiveGroups;
/**
* The LDAP DN used to bind this user.
*/
private Dn bindDn;
/**
* The configuration of the LDAP server that should be used for all queries
* related to this AuthenticatedUser.
*/
private ConnectedLDAPConfiguration config;
/**
* Initializes this AuthenticatedUser with the given credentials,
* connection parameter tokens, and set of effective user groups.
*
* @param config
* The configuration of the LDAP server that should be used for all
* queries related to this AuthenticatedUser.
*
* @param credentials
* The credentials provided when this user was authenticated.
*
* @param tokens
* A Map of all name/value pairs that should be applied as parameter
* tokens when connections are established using the AuthenticatedUser.
*
* @param effectiveGroups
* The unique identifiers of all user groups which affect the
* permissions available to this user.
*/
public void init(UserLDAPConfiguration config, Credentials credentials,
Map<String, String> tokens, Set<String> effectiveGroups) {
this.config = config;
this.credentials = credentials;
this.tokens = Collections.unmodifiableMap(tokens);
this.effectiveGroups = effectiveGroups;
this.bindDn = config.getBindDN();
setIdentifier(config.getGuacamoleUsername());
}
/**
* Returns a Map of all name/value pairs that should be applied as
* parameter tokens when connections are established using this
* AuthenticatedUser.
*
* @return
* A Map of all name/value pairs that should be applied as parameter
* tokens when connections are established using this
* AuthenticatedUser.
*/
public Map<String, String> getTokens() {
return tokens;
}
/**
* Returns the LDAP DN used to bind this user.
*
* @return
* The LDAP DN used to bind this user.
*/
public Dn getBindDn() {
return bindDn;
}
/**
* Returns the configuration of the LDAP server that should be used for all
* queries related to this AuthenticatedUser.
*
* @return
* The configuration of the LDAP server related to this
* AuthenticatedUser.
*/
public ConnectedLDAPConfiguration getLDAPConfiguration() {
return config;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Credentials getCredentials() {
return credentials;
}
@Override
public Set<String> getEffectiveUserGroups() {
return effectiveGroups;
}
@Override
public void invalidate() {
config.close();
}
}

View File

@@ -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.ldap.user;
import com.google.inject.Inject;
import java.util.Collections;
import org.apache.guacamole.auth.ldap.connection.ConnectionService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
import org.apache.guacamole.auth.ldap.group.UserGroupService;
import org.apache.guacamole.net.auth.AbstractUserContext;
import org.apache.guacamole.net.auth.AuthenticationProvider;
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.UserGroup;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.simple.SimpleConnectionGroup;
import org.apache.guacamole.net.auth.simple.SimpleDirectory;
import org.apache.guacamole.net.auth.simple.SimpleObjectPermissionSet;
import org.apache.guacamole.net.auth.simple.SimpleUser;
/**
* An LDAP-specific implementation of UserContext which queries all Guacamole
* connections and users from the LDAP directory.
*/
public class LDAPUserContext extends AbstractUserContext {
/**
* Service for retrieving Guacamole connections from the LDAP server.
*/
@Inject
private ConnectionService connectionService;
/**
* Service for retrieving Guacamole users from the LDAP server.
*/
@Inject
private UserService userService;
/**
* Service for retrieving user groups.
*/
@Inject
private UserGroupService userGroupService;
/**
* Reference to the AuthenticationProvider associated with this
* UserContext.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* Reference to a User object representing the user whose access level
* dictates the users and connections visible through this UserContext.
*/
private User self;
/**
* Directory containing all User objects accessible to the user associated
* with this UserContext.
*/
private Directory<User> userDirectory;
/**
* Directory containing all UserGroup objects accessible to the user
* associated with this UserContext.
*/
private Directory<UserGroup> userGroupDirectory;
/**
* Directory containing all Connection objects accessible to the user
* associated with this UserContext.
*/
private Directory<Connection> connectionDirectory;
/**
* Reference to the root connection group.
*/
private ConnectionGroup rootGroup;
/**
* Initializes this UserContext using the provided AuthenticatedUser.
*
* @param user
* The AuthenticatedUser representing the user that authenticated. This
* user will always have been authenticated via LDAP, as LDAP data is
* not provided to non-LDAP users.
*
* @throws GuacamoleException
* If associated data stored within the LDAP directory cannot be
* queried due to an error.
*/
public void init(LDAPAuthenticatedUser user) throws GuacamoleException {
// Query all accessible users
userDirectory = new SimpleDirectory<>(
userService.getUsers(user)
);
// Query all accessible user groups
userGroupDirectory = new SimpleDirectory<>(
userGroupService.getUserGroups(user)
);
// Query all accessible connections
connectionDirectory = new SimpleDirectory<>(
connectionService.getConnections(user)
);
// Root group contains only connections
rootGroup = new SimpleConnectionGroup(
LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP,
LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP,
connectionDirectory.getIdentifiers(),
Collections.<String>emptyList()
);
// Init self with basic permissions
self = new SimpleUser(user.getIdentifier()) {
@Override
public ObjectPermissionSet getUserPermissions() throws GuacamoleException {
return new SimpleObjectPermissionSet(userDirectory.getIdentifiers());
}
@Override
public ObjectPermissionSet getUserGroupPermissions() throws GuacamoleException {
return new SimpleObjectPermissionSet(userGroupDirectory.getIdentifiers());
}
@Override
public ObjectPermissionSet getConnectionPermissions() throws GuacamoleException {
return new SimpleObjectPermissionSet(connectionDirectory.getIdentifiers());
}
@Override
public ObjectPermissionSet getConnectionGroupPermissions() throws GuacamoleException {
return new SimpleObjectPermissionSet(Collections.singleton(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP));
}
};
}
@Override
public User self() {
return self;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Directory<User> getUserDirectory() throws GuacamoleException {
return userDirectory;
}
@Override
public Directory<UserGroup> getUserGroupDirectory() throws GuacamoleException {
return userGroupDirectory;
}
@Override
public Directory<Connection> getConnectionDirectory()
throws GuacamoleException {
return connectionDirectory;
}
@Override
public ConnectionGroup getRootConnectionGroup() throws GuacamoleException {
return rootGroup;
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.user;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration;
import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration;
/**
* LDAPConfiguration implementation that represents the configuration and
* network connection of an LDAP that has been bound on behalf of a Guacamole
* user.
*/
public class UserLDAPConfiguration extends ConnectedLDAPConfiguration {
/**
* The username of the associated Guacamole user.
*/
private final String username;
/**
* Creates a new UserLDAPConfiguration that associates the given
* LDAPConfiguration of an LDAP server with the active network connection to
* that server, as well as the username of the Guacamole user on behalf of
* whom that connection was established. All functions inherited from the
* LDAPConfiguration interface are delegated to the given LDAPConfiguration.
* It is the responsibility of the caller to ensure the provided
* LdapNetworkConnection is closed after it is no longer needed.
*
* @param config
* The LDAPConfiguration to wrap.
*
* @param username
* The username of the associated Guacamole user.
*
* @param bindDn
* The LDAP DN that was used to bind with the LDAP server to produce
* the given LdapNetworkConnection.
*
* @param connection
* The connection to the LDAP server represented by the given
* configuration.
*/
public UserLDAPConfiguration(LDAPConfiguration config,
String username, Dn bindDn, LdapNetworkConnection connection) {
super(config, bindDn, connection);
this.username = username;
}
/**
* Returns the username of the Guacamole user on behalf of whom the
* associated LDAP network connection was established.
*
* @return
* The username of the associated Guacamole user.
*/
public String getGuacamoleUsername() {
return username;
}
}

View File

@@ -0,0 +1,209 @@
/*
* 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.user;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.ConnectedLDAPConfiguration;
import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
import org.apache.guacamole.auth.ldap.conf.LDAPConfiguration;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.simple.SimpleUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for queries the users visible to a particular Guacamole user
* according to an LDAP directory.
*/
public class UserService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* Service for executing LDAP queries.
*/
@Inject
private ObjectQueryService queryService;
/**
* Returns all Guacamole users accessible to the given user.
*
* @param user
* The AuthenticatedUser object associated with the user who is
* currently authenticated with Guacamole.
*
* @return
* All users accessible to the user currently bound under the given
* LDAP connection, as a map of connection identifier to corresponding
* user object.
*
* @throws GuacamoleException
* If an error occurs preventing retrieval of users.
*/
public Map<String, User> getUsers(LDAPAuthenticatedUser user)
throws GuacamoleException {
ConnectedLDAPConfiguration config = user.getLDAPConfiguration();
// Retrieve all visible user objects
Collection<String> usernameAttrs = config.getUsernameAttributes();
Collection<String> attributes = new HashSet<>(usernameAttrs);
attributes.addAll(config.getAttributes());
List<Entry> results = queryService.search(
config,
config.getLDAPConnection(),
config.getUserBaseDN(),
config.getUserSearchFilter(),
usernameAttrs,
null,
attributes
);
// Convert retrieved users to map of identifier to Guacamole user object
return queryService.asMap(results, entry -> {
// Get username from record
try {
String username = queryService.getIdentifier(entry, attributes);
if (username == null) {
logger.warn("User \"{}\" is missing a username attribute "
+ "and will be ignored.", entry.getDn().toString());
return null;
}
return new SimpleUser(username);
}
catch (LdapInvalidAttributeValueException e) {
return null;
}
});
}
/**
* Returns a list of all DNs corresponding to the users having the given
* username. If multiple username attributes are defined, or if uniqueness
* is not enforced across the username attribute, it is possible that this
* will return multiple DNs.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param ldapConnection
* The connection to the LDAP server to use when querying user DNs.
*
* @param username
* The username of the user whose corresponding user account DNs are
* to be retrieved.
*
* @return
* A list of all DNs corresponding to the users having the given
* username. If no such DNs exist, this list will be empty.
*
* @throws GuacamoleException
* If an error occurs while querying the user DNs, or if the username
* attribute property cannot be parsed within guacamole.properties.
*/
public List<Dn> getUserDNs(LDAPConfiguration config, LdapNetworkConnection ldapConnection,
String username) throws GuacamoleException {
// Retrieve user objects having a matching username
List<Entry> results = queryService.search(config, ldapConnection,
config.getUserBaseDN(),
config.getUserSearchFilter(),
config.getUsernameAttributes(),
username,
Collections.singletonList("dn"));
// Build list of all DNs for retrieved users
List<Dn> userDNs = new ArrayList<>(results.size());
results.forEach(entry -> userDNs.add(entry.getDn()));
return userDNs;
}
/**
* Determines the DN which corresponds to the user having the given
* username. The DN will either be derived directly from the user base DN,
* or queried from the LDAP server, depending on how LDAP authentication
* has been configured.
*
* @param config
* The configuration of the LDAP server being queried.
*
* @param username
* The username of the user whose corresponding DN should be returned.
*
* @return
* The DN which corresponds to the user having the given username.
*
* @throws GuacamoleException
* If required properties are missing, and thus the user DN cannot be
* determined.
*/
public Dn deriveUserDN(LDAPConfiguration config, String username)
throws GuacamoleException {
// Pull username attributes from properties
List<String> usernameAttributes = new ArrayList<>(config.getUsernameAttributes());
// We need exactly one base DN to derive the user DN
if (usernameAttributes.size() != 1) {
logger.warn(String.format("Cannot directly derive user DN when "
+ "multiple username attributes are specified. Please "
+ "define an LDAP search DN using the \"%s\" property "
+ "in your \"guacamole.properties\".",
LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN.getName()));
return null;
}
// Derive user DN from base DN
try {
return new Dn(new Rdn(usernameAttributes.get(0), username),
config.getUserBaseDN());
}
catch (LdapInvalidAttributeValueException | LdapInvalidDnException e) {
throw new GuacamoleServerException("Error trying to derive user DN.", e);
}
}
}

View File

@@ -0,0 +1,18 @@
{
"guacamoleVersion" : "1.6.0",
"name" : "LDAP Authentication",
"namespace" : "ldap",
"authProviders" : [
"org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider"
],
"translations" : [
"translations/de.json",
"translations/en.json",
"translations/zh.json"
]
}

View File

@@ -0,0 +1,7 @@
{
"DATA_SOURCE_LDAP" : {
"NAME" : "LDAP"
}
}

View File

@@ -0,0 +1,7 @@
{
"DATA_SOURCE_LDAP" : {
"NAME" : "轻型目录访问协议"
}
}