Merge 1.1.0 changes back to master.

This commit is contained in:
Michael Jumper
2019-08-11 15:54:36 -07:00
25 changed files with 880 additions and 857 deletions

View File

@@ -141,11 +141,17 @@
<scope>provided</scope>
</dependency>
<!-- JLDAP -->
<!-- Apache Directory LDAP API -->
<dependency>
<groupId>com.novell.ldap</groupId>
<artifactId>jldap</artifactId>
<version>4.3</version>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-all</artifactId>
<version>2.0.0.AM4</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Guice -->

View File

@@ -220,6 +220,15 @@ AOP Alliance (http://aopalliance.sourceforge.net/)
Public Domain (bundled/aopalliance-1.0/LICENSE)
Apache Directory LDAP API (http://directory.apache.org)
-------------------------------------------------------
Version: 2.0.0.AM4
From: 'Apache Software Foundation' (http://apache.org)
License(s):
Apache v2.0 (bundled/directory-api-2.0.0/LICENSE-2.0.txt)
Google Guice (https://github.com/google/guice)
----------------------------------------------
@@ -229,20 +238,6 @@ Google Guice (https://github.com/google/guice)
Apache v2.0 (bundled/guice-3.0/COPYING)
JLDAP (http://www.openldap.org/jldap/)
--------------------------------------
Version: 4.3
From: 'The OpenLDAP Foundation' (http://www.openldap.org/)
License(s):
OpenLDAP Public License v2.8 (bundled/jldap-4.3/LICENSE)
OpenLDAP Public License v2.0.1 (bundled/jldap-4.3/LICENSE-2.0.1)
NOTE: JLDAP is *NOT* dual-licensed. Whether a particular source file is under
version 2.8 or 2.0.1 of the OpenLDAP Public License depends on the license
header of the file in question.
JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
----------------------------------------------------------------------------

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed 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.

View File

@@ -1,47 +0,0 @@
The OpenLDAP Public License
Version 2.8, 17 August 2003
Redistribution and use of this software and associated documentation
("Software"), with or without modification, are permitted provided
that the following conditions are met:
1. Redistributions in source form must retain copyright statements
and notices,
2. Redistributions in binary form must reproduce applicable copyright
statements and notices, this list of conditions, and the following
disclaimer in the documentation and/or other materials provided
with the distribution, and
3. Redistributions must contain a verbatim copy of this document.
The OpenLDAP Foundation may revise this license from time to time.
Each revision is distinguished by a version number. You may use
this Software under terms of this license revision or under the
terms of any subsequent revision of the license.
THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS
CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S)
OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
The names of the authors and copyright holders must not be used in
advertising or otherwise to promote the sale, use or other dealing
in this Software without specific, written prior permission. Title
to copyright in this Software shall at all times remain with copyright
holders.
OpenLDAP is a registered trademark of the OpenLDAP Foundation.
Copyright 1999-2003 The OpenLDAP Foundation, Redwood City,
California, USA. All Rights Reserved. Permission to copy and
distribute verbatim copies of this document is granted.

View File

@@ -1,56 +0,0 @@
A number of files contained in OpenLDAP Software contain
a statement:
USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF
WHICH IS AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR
IN THE FILE "LICENSE" IN THE TOP-LEVEL DIRECTORY OF THE
DISTRIBUTION.
The following is a verbatim copy of version 2.0.1 of the OpenLDAP
Public License referenced in the above statement.
The OpenLDAP Public License
Version 2.0.1, 21 December 1999
Copyright 1999, The OpenLDAP Foundation, Redwood City, California, USA.
All Rights Reserved.
Redistribution and use of this software and associated documentation
("Software"), with or without modification, are permitted provided
that the following conditions are met:
1. Redistributions of source code must retain copyright
statements and notices. Redistributions must also contain a
copy of this document.
2. Redistributions in binary form must reproduce the
above copyright notice, this list of conditions and the
following disclaimer in the documentation and/or other
materials provided with the distribution.
3. The name "OpenLDAP" must not be used to endorse or promote
products derived from this Software without prior written
permission of the OpenLDAP Foundation. For written permission,
please contact foundation@openldap.org.
4. Products derived from this Software may not be called "OpenLDAP"
nor may "OpenLDAP" appear in their names without prior written
permission of the OpenLDAP Foundation. OpenLDAP is a trademark
of the OpenLDAP Foundation.
5. Due credit should be given to the OpenLDAP Project
(http://www.openldap.org/).
THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
THE OPENLDAP FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -21,27 +21,29 @@ package org.apache.guacamole.auth.ldap;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPAttributeSet;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
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 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.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.apache.guacamole.token.TokenName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -113,16 +115,15 @@ public class AuthenticationProviderService {
* If required properties are missing, and thus the user DN cannot be
* determined.
*/
private String getUserBindDN(String username)
throws GuacamoleException {
private Dn getUserBindDN(String username) throws GuacamoleException {
// If a search DN is provided, search the LDAP directory for the DN
// corresponding to the given username
String searchBindDN = confService.getSearchBindDN();
Dn searchBindDN = confService.getSearchBindDN();
if (searchBindDN != null) {
// Create an LDAP connection using the search account
LDAPConnection searchConnection = ldapService.bindAs(
LdapNetworkConnection searchConnection = ldapService.bindAs(
searchBindDN,
confService.getSearchBindPassword()
);
@@ -136,7 +137,7 @@ public class AuthenticationProviderService {
try {
// Retrieve all DNs associated with the given username
List<String> userDNs = userService.getUserDNs(searchConnection, username);
List<Dn> userDNs = userService.getUserDNs(searchConnection, username);
if (userDNs.isEmpty())
return null;
@@ -163,53 +164,6 @@ public class AuthenticationProviderService {
}
/**
* Binds to the LDAP server using the provided Guacamole credentials. The
* DN of the user is derived using the LDAP configuration properties
* provided in guacamole.properties, as is the server hostname and port
* information.
*
* @param credentials
* The credentials to use to bind to the LDAP server.
*
* @return
* A bound LDAP connection, or null if the connection could not be
* bound.
*
* @throws GuacamoleException
* If an error occurs while binding to the LDAP server.
*/
private LDAPConnection bindAs(Credentials credentials)
throws GuacamoleException {
// Get username and password from credentials
String username = credentials.getUsername();
String password = credentials.getPassword();
// Require username
if (username == null || username.isEmpty()) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null;
}
// Require password, and do not allow anonymous binding
if (password == null || password.isEmpty()) {
logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
return null;
}
// Determine user DN
String userDN = getUserBindDN(username);
if (userDN == null) {
logger.debug("Unable to determine DN for user \"{}\".", username);
return null;
}
// Bind using user's DN
return ldapService.bindAs(userDN, password);
}
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials. Also adds custom LDAP attributes to the
@@ -228,39 +182,40 @@ public class AuthenticationProviderService {
*/
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);
}
Dn bindDn = getUserBindDN(username);
if (bindDn == null || bindDn.isEmpty()) {
throw new GuacamoleInvalidCredentialsException("Unable to determine"
+ " DN of user " + username, CredentialsInfo.USERNAME_PASSWORD);
}
// Attempt bind
LDAPConnection ldapConnection;
try {
ldapConnection = bindAs(credentials);
}
catch (GuacamoleException e) {
logger.error("Cannot bind with LDAP server: {}", e.getMessage());
logger.debug("Error binding with LDAP server.", e);
ldapConnection = null;
}
LdapNetworkConnection ldapConnection = ldapService.bindAs(bindDn, password);
// Retrieve group membership of the user that just authenticated
Set<String> effectiveGroups =
userGroupService.getParentUserGroupIdentifiers(ldapConnection,
bindDn);
// If bind fails, permission to login is denied
if (ldapConnection == null)
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
try {
// Retrieve group membership of the user that just authenticated
Set<String> effectiveGroups =
userGroupService.getParentUserGroupIdentifiers(ldapConnection,
ldapConnection.getAuthenticationDN());
// Return AuthenticatedUser if bind succeeds
LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(credentials, getAttributeTokens(ldapConnection, credentials.getUsername()), effectiveGroups);
return authenticatedUser;
}
// Always disconnect
finally {
ldapService.disconnect(ldapConnection);
}
// Return AuthenticatedUser if bind succeeds
LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(credentials, getAttributeTokens(ldapConnection,
bindDn), effectiveGroups, bindDn);
return authenticatedUser;
}
@@ -286,8 +241,8 @@ public class AuthenticationProviderService {
* @throws GuacamoleException
* If an error occurs retrieving the user DN or the attributes.
*/
private Map<String, String> getAttributeTokens(LDAPConnection ldapConnection,
String username) throws GuacamoleException {
private Map<String, String> getAttributeTokens(LdapNetworkConnection ldapConnection,
Dn userDn) throws GuacamoleException {
// Get attributes from configuration information
List<String> attrList = confService.getAttributes();
@@ -298,29 +253,27 @@ public class AuthenticationProviderService {
// Build LDAP query parameters
String[] attrArray = attrList.toArray(new String[attrList.size()]);
String userDN = getUserBindDN(username);
Map<String, String> tokens = new HashMap<>();
try {
// Get LDAP attributes by querying LDAP
LDAPEntry userEntry = ldapConnection.read(userDN, attrArray);
Entry userEntry = ldapConnection.lookup(userDn, attrArray);
if (userEntry == null)
return Collections.<String, String>emptyMap();
LDAPAttributeSet attrSet = userEntry.getAttributeSet();
if (attrSet == null)
Collection<Attribute> attributes = userEntry.getAttributes();
if (attributes == null)
return Collections.<String, String>emptyMap();
// Convert each retrieved attribute into a corresponding token
for (Object attrObj : attrSet) {
LDAPAttribute attr = (LDAPAttribute)attrObj;
tokens.put(TokenName.canonicalize(attr.getName(),
LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getStringValue());
for (Attribute attr : attributes) {
tokens.put(TokenName.canonicalize(attr.getId(),
LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getString());
}
}
catch (LDAPException e) {
catch (LdapException e) {
throw new GuacamoleServerException("Could not query LDAP user attributes.", e);
}
@@ -347,23 +300,25 @@ public class AuthenticationProviderService {
// Bind using credentials associated with AuthenticatedUser
Credentials credentials = authenticatedUser.getCredentials();
LDAPConnection ldapConnection = bindAs(credentials);
if (ldapConnection == null)
return null;
if (authenticatedUser instanceof LDAPAuthenticatedUser) {
Dn bindDn = ((LDAPAuthenticatedUser) authenticatedUser).getBindDn();
LdapNetworkConnection ldapConnection = ldapService.bindAs(bindDn, credentials.getPassword());
try {
try {
// Build user context by querying LDAP
LDAPUserContext userContext = userContextProvider.get();
userContext.init(authenticatedUser, ldapConnection);
return userContext;
// Build user context by querying LDAP
LDAPUserContext userContext = userContextProvider.get();
userContext.init(authenticatedUser, ldapConnection);
return userContext;
}
// Always disconnect
finally {
ldapService.disconnect(ldapConnection);
}
}
// Always disconnect
finally {
ldapService.disconnect(ldapConnection);
}
return null;
}

View File

@@ -1,74 +0,0 @@
/*
* 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.novell.ldap.LDAPSearchConstraints;
/**
* Data type that handles acceptable values for configuring
* alias dereferencing behavior when querying LDAP servers.
*/
public enum DereferenceAliasesMode {
/**
* Never dereference aliases. This is the default.
*/
NEVER(LDAPSearchConstraints.DEREF_NEVER),
/**
* Aliases are dereferenced below the base object, but not to locate
* the base object itself. So, if the base object is itself an alias
* the search will not complete.
*/
SEARCHING(LDAPSearchConstraints.DEREF_SEARCHING),
/**
* Aliases are only dereferenced to locate the base object, but not
* after that. So, a search against a base object that is an alias will
* find any subordinates of the real object the alias references, but
* further aliases in the search will not be dereferenced.
*/
FINDING(LDAPSearchConstraints.DEREF_FINDING),
/**
* Aliases will always be dereferenced, both to locate the base object
* and when handling results returned by the search.
*/
ALWAYS(LDAPSearchConstraints.DEREF_ALWAYS);
/**
* The integer constant as defined in the JLDAP library that
* the LDAPSearchConstraints class uses to define the
* dereferencing behavior during search operations.
*/
public final int DEREF_VALUE;
/**
* Initializes the dereference aliases object with the integer
* value the setting maps to per the JLDAP implementation.
*
* @param derefValue
* The value associated with this dereference setting
*/
private DereferenceAliasesMode(int derefValue) {
this.DEREF_VALUE = derefValue;
}
}

View File

@@ -1,120 +0,0 @@
/*
* 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;
/**
* Service for escaping LDAP filters, distinguished names (DN's), etc.
*/
public class EscapingService {
/**
* Escapes the given string for use within an LDAP search filter. This
* implementation is provided courtesy of OWASP:
*
* https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
*
* @param filter
* The string to escape such that it has no special meaning within an
* LDAP search filter.
*
* @return
* The escaped string, safe for use within an LDAP search filter.
*/
public String escapeLDAPSearchFilter(String filter) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filter.length(); i++) {
char curChar = filter.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\5c");
break;
case '*':
sb.append("\\2a");
break;
case '(':
sb.append("\\28");
break;
case ')':
sb.append("\\29");
break;
case '\u0000':
sb.append("\\00");
break;
default:
sb.append(curChar);
}
}
return sb.toString();
}
/**
* Escapes the given string such that it is safe for use within an LDAP
* distinguished name (DN). This implementation is provided courtesy of
* OWASP:
*
* https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
*
* @param name
* The string to escape such that it has no special meaning within an
* LDAP DN.
*
* @return
* The escaped string, safe for use within an LDAP DN.
*/
public String escapeDN(String name) {
StringBuilder sb = new StringBuilder();
if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
sb.append('\\'); // add the leading backslash if needed
}
for (int i = 0; i < name.length(); i++) {
char curChar = name.charAt(i);
switch (curChar) {
case '\\':
sb.append("\\\\");
break;
case ',':
sb.append("\\,");
break;
case '+':
sb.append("\\+");
break;
case '"':
sb.append("\\\"");
break;
case '<':
sb.append("\\<");
break;
case '>':
sb.append("\\>");
break;
case ';':
sb.append("\\;");
break;
default:
sb.append(curChar);
}
}
if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
}
return sb.toString();
}
}

View File

@@ -20,6 +20,7 @@
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;
@@ -76,7 +77,6 @@ public class LDAPAuthenticationProviderModule extends AbstractModule {
// Bind LDAP-specific services
bind(ConfigurationService.class);
bind(ConnectionService.class);
bind(EscapingService.class);
bind(LDAPConnectionService.class);
bind(ObjectQueryService.class);
bind(UserGroupService.class);

View File

@@ -20,14 +20,28 @@
package org.apache.guacamole.auth.ldap;
import com.google.inject.Inject;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPConstraints;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPJSSESecureSocketFactory;
import com.novell.ldap.LDAPJSSEStartTLSFactory;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.message.BindRequest;
import org.apache.directory.api.ldap.model.message.BindRequestImpl;
import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
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.LdapConnection;
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.ConfigurationService;
import org.apache.guacamole.auth.ldap.conf.EncryptionMethod;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +53,7 @@ public class LDAPConnectionService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
private static final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -48,19 +62,24 @@ public class LDAPConnectionService {
private ConfigurationService confService;
/**
* Creates a new instance of LDAPConnection, configured as required to use
* whichever encryption method is requested within guacamole.properties.
* Creates a new instance of LdapNetworkConnection, configured as required
* to use whichever encryption method is requested within
* guacamole.properties.
*
* @return
* A new LDAPConnection instance which has already been configured to
* use the encryption method requested within guacamole.properties.
* A new LdapNetworkConnection instance which has already been
* configured to use the encryption method 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 LDAPConnection createLDAPConnection() throws GuacamoleException {
private LdapNetworkConnection createLDAPConnection() throws GuacamoleException {
String host = confService.getServerHostname();
int port = confService.getServerPort();
// Map encryption method to proper connection and socket factory
EncryptionMethod encryptionMethod = confService.getEncryptionMethod();
switch (encryptionMethod) {
@@ -68,17 +87,17 @@ public class LDAPConnectionService {
// Unencrypted LDAP connection
case NONE:
logger.debug("Connection to LDAP server without encryption.");
return new LDAPConnection();
return new LdapNetworkConnection(host, port);
// LDAP over SSL (LDAPS)
case SSL:
logger.debug("Connecting to LDAP server using SSL/TLS.");
return new LDAPConnection(new LDAPJSSESecureSocketFactory());
return new LdapNetworkConnection(host, port, true);
// LDAP + STARTTLS
case STARTTLS:
logger.debug("Connecting to LDAP server using STARTTLS.");
return new LDAPConnection(new LDAPJSSEStartTLSFactory());
return new LdapNetworkConnection(host, port);
// The encryption method, though known, is not actually
// implemented. If encountered, this would be a bug.
@@ -106,86 +125,113 @@ public class LDAPConnectionService {
* @throws GuacamoleException
* If an error occurs while binding to the LDAP server.
*/
public LDAPConnection bindAs(String userDN, String password)
public LdapNetworkConnection bindAs(Dn userDN, String password)
throws GuacamoleException {
// Obtain appropriately-configured LDAPConnection instance
LDAPConnection ldapConnection = createLDAPConnection();
// Configure LDAP connection constraints
LDAPConstraints ldapConstraints = ldapConnection.getConstraints();
if (ldapConstraints == null)
ldapConstraints = new LDAPConstraints();
// Set whether or not we follow referrals
ldapConstraints.setReferralFollowing(confService.getFollowReferrals());
// Set referral authentication to use the provided credentials.
if (userDN != null && !userDN.isEmpty())
ldapConstraints.setReferralHandler(new ReferralAuthHandler(userDN, password));
// Set the maximum number of referrals we follow
ldapConstraints.setHopLimit(confService.getMaxReferralHops());
// Set timelimit to wait for LDAP operations, converting to ms
ldapConstraints.setTimeLimit(confService.getOperationTimeout() * 1000);
// Apply the constraints to the connection
ldapConnection.setConstraints(ldapConstraints);
// Obtain appropriately-configured LdapNetworkConnection instance
LdapNetworkConnection ldapConnection = createLDAPConnection();
try {
// Connect to LDAP server
ldapConnection.connect(
confService.getServerHostname(),
confService.getServerPort()
);
ldapConnection.connect();
// Explicitly start TLS if requested
if (confService.getEncryptionMethod() == EncryptionMethod.STARTTLS)
ldapConnection.startTLS();
ldapConnection.startTls();
}
catch (LDAPException e) {
logger.error("Unable to connect to LDAP server: {}", e.getMessage());
logger.debug("Failed to connect to LDAP server.", e);
return null;
catch (LdapException e) {
throw new GuacamoleServerException("Error connecting to LDAP server.", e);
}
// Bind using provided credentials
try {
byte[] passwordBytes;
try {
// Convert password into corresponding byte array
if (password != null)
passwordBytes = password.getBytes("UTF-8");
else
passwordBytes = null;
}
catch (UnsupportedEncodingException e) {
logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
disconnect(ldapConnection);
return null;
}
// Bind as user
ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, passwordBytes);
BindRequest bindRequest = new BindRequestImpl();
bindRequest.setDn(userDN);
bindRequest.setCredentials(password);
BindResponse bindResponse = ldapConnection.bind(bindRequest);
if (bindResponse.getLdapResult().getResultCode() == ResultCodeEnum.SUCCESS)
return ldapConnection;
else
throw new GuacamoleInvalidCredentialsException("Error binding"
+ " to server: " + bindResponse.toString(),
CredentialsInfo.USERNAME_PASSWORD);
}
// Disconnect if an error occurs during bind
catch (LDAPException e) {
logger.debug("LDAP bind failed.", e);
catch (LdapException e) {
logger.debug("Unable to bind to LDAP server.", e);
disconnect(ldapConnection);
return null;
throw new GuacamoleInvalidCredentialsException(
"Unable to bind to the LDAP server.",
CredentialsInfo.USERNAME_PASSWORD);
}
return ldapConnection;
}
/**
* Generate a new LdapNetworkConnection object for following a referral
* with the given LdapUrl, and copy the username and password
* from the original connection.
*
* @param referralUrl
* The LDAP URL to follow.
*
* @param ldapConfig
* The connection configuration to use to retrieve username and
* password.
*
* @param hop
* The current hop number of this referral - once the configured
* limit is reached, this method will throw an exception.
*
* @return
* A LdapNetworkConnection object that points at the location
* specified in the referralUrl.
*
* @throws GuacamoleException
* If an error occurs parsing out the LdapUrl object or the
* maximum number of referral hops is reached.
*/
public LdapNetworkConnection getReferralConnection(LdapUrl referralUrl,
LdapConnectionConfig ldapConfig, int hop)
throws GuacamoleException {
if (hop >= confService.getMaxReferralHops())
throw new GuacamoleServerException("Maximum number of referrals reached.");
LdapConnectionConfig referralConfig = new LdapConnectionConfig();
// Copy bind name and password from original config
referralConfig.setName(ldapConfig.getName());
referralConfig.setCredentials(ldapConfig.getCredentials());
// Look for host - if not there, bail out.
String host = referralUrl.getHost();
if (host == null || host.isEmpty())
throw new GuacamoleServerException("Referral URL contains no host.");
referralConfig.setLdapHost(host);
// Look for port, or assign a default.
int port = referralUrl.getPort();
if (port < 1)
referralConfig.setLdapPort(389);
else
referralConfig.setLdapPort(port);
// Deal with SSL connections
if (referralUrl.getScheme().equals(LdapUrl.LDAPS_SCHEME))
referralConfig.setUseSsl(true);
else
referralConfig.setUseSsl(false);
return new LdapNetworkConnection(referralConfig);
}
/**
@@ -195,19 +241,53 @@ public class LDAPConnectionService {
* @param ldapConnection
* The LDAP connection to disconnect.
*/
public void disconnect(LDAPConnection ldapConnection) {
public void disconnect(LdapConnection ldapConnection) {
// Attempt disconnect
try {
ldapConnection.disconnect();
ldapConnection.close();
}
// Warn if disconnect unexpectedly fails
catch (LDAPException e) {
catch (IOException e) {
logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage());
logger.debug("LDAP disconnect failed.", e);
}
}
/**
* Generate a SearchRequest object using the given Base DN and filter
* and retrieving other properties from the LDAP configuration service.
*
* @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(Dn baseDn, ExprNode filter)
throws GuacamoleException {
SearchRequest searchRequest = new SearchRequestImpl();
searchRequest.setBase(baseDn);
searchRequest.setDerefAliases(confService.getDereferenceAliases());
searchRequest.setScope(SearchScope.SUBTREE);
searchRequest.setFilter(filter);
searchRequest.setSizeLimit(confService.getMaxResults());
searchRequest.setTimeLimit(confService.getOperationTimeout());
searchRequest.setTypesOnly(false);
if (confService.getFollowReferrals())
searchRequest.followReferrals();
return searchRequest;
}
}

View File

@@ -20,18 +20,28 @@
package org.apache.guacamole.auth.ldap;
import com.google.inject.Inject;
import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPReferralException;
import com.novell.ldap.LDAPSearchResults;
import java.util.ArrayList;
import java.util.Collection;
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.message.Referral;
import org.apache.directory.api.ldap.model.message.SearchRequest;
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.net.auth.Identifiable;
@@ -50,19 +60,13 @@ public class ObjectQueryService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
private static final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
/**
* Service for escaping parts of LDAP queries.
* Service for connecting to LDAP directory.
*/
@Inject
private EscapingService escapingService;
/**
* Service for retrieving LDAP server configuration information.
*/
@Inject
private ConfigurationService confService;
private LDAPConnectionService ldapService;
/**
* Returns the identifier of the object represented by the given LDAP
@@ -86,14 +90,18 @@ public class ObjectQueryService {
* 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(LDAPEntry entry, Collection<String> attributes) {
public String getIdentifier(Entry entry, Collection<String> attributes)
throws LdapInvalidAttributeValueException {
// Retrieve the first value of the highest priority identifier attribute
for (String identifierAttribute : attributes) {
LDAPAttribute identifier = entry.getAttribute(identifierAttribute);
Attribute identifier = entry.get(identifierAttribute);
if (identifier != null)
return identifier.getStringValue();
return identifier.getString();
}
// No identifier attribute is present on the entry
@@ -125,42 +133,25 @@ public class ObjectQueryService {
* An LDAP query which will search for arbitrary LDAP objects having at
* least one of the given attributes set to the specified value.
*/
public String generateQuery(String filter,
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
StringBuilder ldapQuery = new StringBuilder();
ldapQuery.append("(&");
ldapQuery.append(filter);
AndNode searchFilter = new AndNode();
searchFilter.addNode(filter);
// Include all attributes within OR clause if there are more than one
if (attributes.size() > 1)
ldapQuery.append("(|");
OrNode attributeFilter = new OrNode();
// Add equality comparison for each possible attribute
for (String attribute : attributes) {
ldapQuery.append("(");
ldapQuery.append(escapingService.escapeLDAPSearchFilter(attribute));
attributes.forEach(attribute ->
attributeFilter.addNode(new EqualityNode(attribute, attributeValue))
);
if (attributeValue != null) {
ldapQuery.append("=");
ldapQuery.append(escapingService.escapeLDAPSearchFilter(attributeValue));
ldapQuery.append(")");
}
else
ldapQuery.append("=*)");
}
// Close OR clause, if any
if (attributes.size() > 1)
ldapQuery.append(")");
// Close overall query (AND clause)
ldapQuery.append(")");
return ldapQuery.toString();
searchFilter.addNode(attributeFilter);
return searchFilter;
}
@@ -178,6 +169,10 @@ public class ObjectQueryService {
*
* @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.
*
* @return
* A list of all results accessible to the user currently bound under
@@ -188,51 +183,50 @@ public class ObjectQueryService {
* information required to execute the query cannot be read from
* guacamole.properties.
*/
public List<LDAPEntry> search(LDAPConnection ldapConnection,
String baseDN, String query) throws GuacamoleException {
public List<Entry> search(LdapNetworkConnection ldapConnection,
Dn baseDN, ExprNode query, int searchHop) throws GuacamoleException {
logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query);
try {
LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
// Search within subtree of given base DN
LDAPSearchResults results = ldapConnection.search(baseDN,
LDAPConnection.SCOPE_SUB, query, null, false,
confService.getLDAPSearchConstraints());
SearchRequest request = ldapService.getSearchRequest(baseDN,
query);
SearchCursor results = ldapConnection.search(request);
// Produce list of all entries in the search result, automatically
// following referrals if configured to do so
List<LDAPEntry> entries = new ArrayList<>(results.getCount());
while (results.hasMore()) {
List<Entry> entries = new ArrayList<>();
while (results.next()) {
try {
entries.add(results.next());
if (results.isEntry()) {
entries.add(results.getEntry());
}
// Warn if referrals cannot be followed
catch (LDAPReferralException e) {
if (confService.getFollowReferrals()) {
logger.error("Could not follow referral: {}", e.getFailedReferral());
logger.debug("Error encountered trying to follow referral.", e);
throw new GuacamoleServerException("Could not follow LDAP referral.", e);
}
else {
logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage());
logger.debug("Got a referral, but configured to not follow them.", e);
else if (results.isReferral() && request.isFollowReferrals()) {
Referral referral = results.getReferral();
for (String url : referral.getLdapUrls()) {
LdapNetworkConnection referralConnection =
ldapService.getReferralConnection(
new LdapUrl(url),
ldapConnectionConfig, searchHop++
);
entries.addAll(search(referralConnection, baseDN, query,
searchHop));
}
}
catch (LDAPException e) {
logger.warn("Failed to process an LDAP search result. Error was: {}", e.resultCodeToString());
logger.debug("Error processing LDAPEntry search result.", e);
}
}
return entries;
}
catch (LDAPException | GuacamoleException e) {
catch (CursorException | LdapException e) {
throw new GuacamoleServerException("Unable to query list of "
+ "objects from LDAP directory.", e);
}
@@ -274,11 +268,11 @@ public class ObjectQueryService {
* information required to execute the query cannot be read from
* guacamole.properties.
*/
public List<LDAPEntry> search(LDAPConnection ldapConnection, String baseDN,
String filter, Collection<String> attributes, String attributeValue)
public List<Entry> search(LdapNetworkConnection ldapConnection, Dn baseDN,
ExprNode filter, Collection<String> attributes, String attributeValue)
throws GuacamoleException {
String query = generateQuery(filter, attributes, attributeValue);
return search(ldapConnection, baseDN, query);
ExprNode query = generateQuery(filter, attributes, attributeValue);
return search(ldapConnection, baseDN, query, 0);
}
/**
@@ -302,15 +296,15 @@ public class ObjectQueryService {
* {@link Map} under its corresponding identifier.
*/
public <ObjectType extends Identifiable> Map<String, ObjectType>
asMap(List<LDAPEntry> entries, Function<LDAPEntry, ObjectType> mapper) {
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 (LDAPEntry entry : entries) {
for (Entry entry : entries) {
ObjectType object = mapper.apply(entry);
if (object == null) {
logger.debug("Ignoring object \"{}\".", entry.getDN());
logger.debug("Ignoring object \"{}\".", entry.getDn().toString());
continue;
}
@@ -320,7 +314,7 @@ public class ObjectQueryService {
if (objects.putIfAbsent(identifier, object) != null)
logger.warn("Multiple objects ambiguously map to the "
+ "same identifier (\"{}\"). Ignoring \"{}\" as "
+ "a duplicate.", identifier, entry.getDN());
+ "a duplicate.", identifier, entry.getDn().toString());
}

View File

@@ -1,79 +0,0 @@
/*
* 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.novell.ldap.LDAPAuthHandler;
import com.novell.ldap.LDAPAuthProvider;
import java.io.UnsupportedEncodingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class that implements the necessary authentication handling
* for following referrals in LDAP connections.
*/
public class ReferralAuthHandler implements LDAPAuthHandler {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ReferralAuthHandler.class);
/**
* The LDAPAuthProvider object that will be set and returned to the referral handler.
*/
private final LDAPAuthProvider ldapAuth;
/**
* Creates a ReferralAuthHandler object to handle authentication when
* following referrals in a LDAP connection, using the provided dn and
* password.
*
* @param dn
* The distinguished name to use for the referral login.
*
* @param password
* The password to use for the referral login.
*/
public ReferralAuthHandler(String dn, String password) {
byte[] passwordBytes;
try {
// Convert password into corresponding byte array
if (password != null)
passwordBytes = password.getBytes("UTF-8");
else
passwordBytes = null;
}
catch (UnsupportedEncodingException e) {
logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
}
ldapAuth = new LDAPAuthProvider(dn, passwordBytes);
}
@Override
public LDAPAuthProvider getAuthProvider(String host, int port) {
return ldapAuth;
}
}

View File

@@ -17,27 +17,23 @@
* under the License.
*/
package org.apache.guacamole.auth.ldap;
package org.apache.guacamole.auth.ldap.conf;
import com.google.inject.Inject;
import com.novell.ldap.LDAPSearchConstraints;
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.environment.Environment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for retrieving configuration information regarding the LDAP server.
*/
public class ConfigurationService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ConfigurationService.class);
/**
* The Guacamole server environment.
*/
@@ -113,7 +109,7 @@ public class ConfigurationService {
* If guacamole.properties cannot be parsed, or if the user base DN
* property is not specified.
*/
public String getUserBaseDN() throws GuacamoleException {
public Dn getUserBaseDN() throws GuacamoleException {
return environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USER_BASE_DN
);
@@ -132,7 +128,7 @@ public class ConfigurationService {
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getConfigurationBaseDN() throws GuacamoleException {
public Dn getConfigurationBaseDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN
);
@@ -168,7 +164,7 @@ public class ConfigurationService {
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getGroupBaseDN() throws GuacamoleException {
public Dn getGroupBaseDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN
);
@@ -187,7 +183,7 @@ public class ConfigurationService {
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getSearchBindDN() throws GuacamoleException {
public Dn getSearchBindDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN
);
@@ -242,7 +238,7 @@ public class ConfigurationService {
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private int getMaxResults() throws GuacamoleException {
public int getMaxResults() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS,
1000
@@ -262,10 +258,10 @@ public class ConfigurationService {
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private DereferenceAliasesMode getDereferenceAliases() throws GuacamoleException {
public AliasDerefMode getDereferenceAliases() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES,
DereferenceAliasesMode.NEVER
AliasDerefMode.NEVER_DEREF_ALIASES
);
}
@@ -288,28 +284,8 @@ public class ConfigurationService {
}
/**
* Returns a set of LDAPSearchConstraints to apply globally
* to all LDAP searches.
*
* @return
* A LDAPSearchConstraints object containing constraints
* to be applied to all LDAP search operations.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public LDAPSearchConstraints getLDAPSearchConstraints() throws GuacamoleException {
LDAPSearchConstraints constraints = new LDAPSearchConstraints();
constraints.setMaxResults(getMaxResults());
constraints.setDereference(getDereferenceAliases().DEREF_VALUE);
return constraints;
}
/**
* Returns the maximum number of referral hops to follow.
* Returns the maximum number of referral hops to follow. By default
* a maximum of 5 hops is allowed.
*
* @return
* The maximum number of referral hops to follow
@@ -328,20 +304,20 @@ public class ConfigurationService {
/**
* Returns the search filter that should be used when querying the
* LDAP server for Guacamole users. If no filter is specified,
* a default of "(objectClass=*)" is returned.
* a default of "(objectClass=user)" is returned.
*
* @return
* The search filter that should be used when querying the
* LDAP server for users that are valid in Guacamole, or
* "(objectClass=*)" if not specified.
* "(objectClass=user)" if not specified.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public String getUserSearchFilter() throws GuacamoleException {
public ExprNode getUserSearchFilter() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER,
"(objectClass=*)"
new PresenceNode("objectClass")
);
}
@@ -363,7 +339,8 @@ public class ConfigurationService {
}
/**
* Returns names for custom LDAP user attributes.
* Returns names for custom LDAP user attributes. By default no
* attributes will be returned.
*
* @return
* Custom LDAP user attributes as configured in guacamole.properties.

View File

@@ -17,21 +17,22 @@
* under the License.
*/
package org.apache.guacamole.auth.ldap;
package org.apache.guacamole.auth.ldap.conf;
import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty with a value of DereferenceAliases. The possible strings
* "never", "searching", "finding", and "always" are mapped to their values as a
* DereferenceAliases enum. Anything else results in a parse error.
* A GuacamoleProperty with a value of AliasDerefMode. The possible strings
* "never", "searching", "finding", and "always" are mapped to their values as
* an AliasDerefMode object. Anything else results in a parse error.
*/
public abstract class DereferenceAliasesProperty implements GuacamoleProperty<DereferenceAliasesMode> {
public abstract class DereferenceAliasesProperty implements GuacamoleProperty<AliasDerefMode> {
@Override
public DereferenceAliasesMode parseValue(String value) throws GuacamoleException {
public AliasDerefMode parseValue(String value) throws GuacamoleException {
// No value provided, so return null.
if (value == null)
@@ -39,19 +40,19 @@ public abstract class DereferenceAliasesProperty implements GuacamoleProperty<De
// Never dereference aliases
if (value.equals("never"))
return DereferenceAliasesMode.NEVER;
return AliasDerefMode.NEVER_DEREF_ALIASES;
// Dereference aliases during search operations, but not at base
if (value.equals("searching"))
return DereferenceAliasesMode.SEARCHING;
return AliasDerefMode.DEREF_IN_SEARCHING;
// Dereference aliases to locate base, but not during searches
if (value.equals("finding"))
return DereferenceAliasesMode.FINDING;
return AliasDerefMode.DEREF_FINDING_BASE_OBJ;
// Always dereference aliases
if (value.equals("always"))
return DereferenceAliasesMode.ALWAYS;
return AliasDerefMode.DEREF_ALWAYS;
// Anything else is invalid and results in an error
throw new GuacamoleServerException("Dereference aliases must be one of \"never\", \"searching\", \"finding\", or \"always\".");

View File

@@ -17,7 +17,7 @@
* under the License.
*/
package org.apache.guacamole.auth.ldap;
package org.apache.guacamole.auth.ldap.conf;
/**
* All possible encryption methods which may be used when connecting to an LDAP

View File

@@ -17,7 +17,7 @@
* under the License.
*/
package org.apache.guacamole.auth.ldap;
package org.apache.guacamole.auth.ldap.conf;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;

View File

@@ -17,7 +17,7 @@
* under the License.
*/
package org.apache.guacamole.auth.ldap;
package org.apache.guacamole.auth.ldap.conf;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
@@ -39,7 +39,8 @@ public class LDAPGuacamoleProperties {
/**
* The base DN to search for Guacamole configurations.
*/
public static final StringGuacamoleProperty LDAP_CONFIG_BASE_DN = new StringGuacamoleProperty() {
public static final LdapDnGuacamoleProperty LDAP_CONFIG_BASE_DN =
new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-config-base-dn"; }
@@ -52,7 +53,8 @@ public class LDAPGuacamoleProperties {
* 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 StringGuacamoleProperty LDAP_USER_BASE_DN = new StringGuacamoleProperty() {
public static final LdapDnGuacamoleProperty LDAP_USER_BASE_DN =
new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-user-base-dn"; }
@@ -64,7 +66,8 @@ public class LDAPGuacamoleProperties {
* will be used for RBAC must be contained somewhere within the subtree of
* this DN.
*/
public static final StringGuacamoleProperty LDAP_GROUP_BASE_DN = new StringGuacamoleProperty() {
public static final LdapDnGuacamoleProperty LDAP_GROUP_BASE_DN =
new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-group-base-dn"; }
@@ -79,7 +82,8 @@ public class LDAPGuacamoleProperties {
* 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 StringListProperty LDAP_USERNAME_ATTRIBUTE = new StringListProperty() {
public static final StringListProperty LDAP_USERNAME_ATTRIBUTE =
new StringListProperty() {
@Override
public String getName() { return "ldap-username-attribute"; }
@@ -91,7 +95,8 @@ public class LDAPGuacamoleProperties {
* attributes must be present within each Guacamole user group's record in
* the LDAP directory for that group to be visible.
*/
public static final StringListProperty LDAP_GROUP_NAME_ATTRIBUTE = new StringListProperty() {
public static final StringListProperty LDAP_GROUP_NAME_ATTRIBUTE =
new StringListProperty() {
@Override
public String getName() { return "ldap-group-name-attribute"; }
@@ -101,7 +106,8 @@ public class LDAPGuacamoleProperties {
/**
* The port on the LDAP server to connect to when authenticating users.
*/
public static final IntegerGuacamoleProperty LDAP_PORT = new IntegerGuacamoleProperty() {
public static final IntegerGuacamoleProperty LDAP_PORT =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-port"; }
@@ -111,7 +117,8 @@ public class LDAPGuacamoleProperties {
/**
* The hostname of the LDAP server to connect to when authenticating users.
*/
public static final StringGuacamoleProperty LDAP_HOSTNAME = new StringGuacamoleProperty() {
public static final StringGuacamoleProperty LDAP_HOSTNAME =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-hostname"; }
@@ -124,7 +131,8 @@ public class LDAPGuacamoleProperties {
* specified, the DNs of users attempting to log in will be derived from
* the LDAP_BASE_DN and LDAP_USERNAME_ATTRIBUTE directly.
*/
public static final StringGuacamoleProperty LDAP_SEARCH_BIND_DN = new StringGuacamoleProperty() {
public static final LdapDnGuacamoleProperty LDAP_SEARCH_BIND_DN =
new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-search-bind-dn"; }
@@ -137,7 +145,8 @@ public class LDAPGuacamoleProperties {
* 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() {
public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-search-bind-password"; }
@@ -149,7 +158,8 @@ public class LDAPGuacamoleProperties {
* The chosen method will also dictate the default port if not already
* explicitly specified via LDAP_PORT.
*/
public static final EncryptionMethodProperty LDAP_ENCRYPTION_METHOD = new EncryptionMethodProperty() {
public static final EncryptionMethodProperty LDAP_ENCRYPTION_METHOD =
new EncryptionMethodProperty() {
@Override
public String getName() { return "ldap-encryption-method"; }
@@ -159,7 +169,8 @@ public class LDAPGuacamoleProperties {
/**
* The maximum number of results a LDAP query can return.
*/
public static final IntegerGuacamoleProperty LDAP_MAX_SEARCH_RESULTS = new IntegerGuacamoleProperty() {
public static final IntegerGuacamoleProperty LDAP_MAX_SEARCH_RESULTS =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-max-search-results"; }
@@ -170,7 +181,8 @@ public class LDAPGuacamoleProperties {
* Property that controls whether or not the LDAP connection follows
* (dereferences) aliases as it searches the tree.
*/
public static final DereferenceAliasesProperty LDAP_DEREFERENCE_ALIASES = new DereferenceAliasesProperty() {
public static final DereferenceAliasesProperty LDAP_DEREFERENCE_ALIASES =
new DereferenceAliasesProperty() {
@Override
public String getName() { return "ldap-dereference-aliases"; }
@@ -180,7 +192,8 @@ public class LDAPGuacamoleProperties {
/**
* A search filter to apply to user LDAP queries.
*/
public static final StringGuacamoleProperty LDAP_USER_SEARCH_FILTER = new StringGuacamoleProperty() {
public static final LdapFilterGuacamoleProperty LDAP_USER_SEARCH_FILTER =
new LdapFilterGuacamoleProperty() {
@Override
public String getName() { return "ldap-user-search-filter"; }
@@ -190,7 +203,8 @@ public class LDAPGuacamoleProperties {
/**
* Whether or not we should follow referrals.
*/
public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS = new BooleanGuacamoleProperty() {
public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "ldap-follow-referrals"; }
@@ -200,7 +214,8 @@ public class LDAPGuacamoleProperties {
/**
* Maximum number of referral hops to follow.
*/
public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS = new IntegerGuacamoleProperty() {
public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-max-referral-hops"; }
@@ -210,7 +225,8 @@ public class LDAPGuacamoleProperties {
/**
* Number of seconds to wait for LDAP operations to complete.
*/
public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT = new IntegerGuacamoleProperty() {
public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-operation-timeout"; }
@@ -221,7 +237,8 @@ public class LDAPGuacamoleProperties {
* Custom attribute or attributes to query from Guacamole user's record in
* the LDAP directory.
*/
public static final StringListProperty LDAP_USER_ATTRIBUTES = new StringListProperty() {
public static final StringListProperty LDAP_USER_ATTRIBUTES =
new StringListProperty() {
@Override
public String getName() { return "ldap-user-attributes"; }
@@ -231,7 +248,8 @@ public class LDAPGuacamoleProperties {
/**
* LDAP attribute used to enumerate members of a group in the LDAP directory.
*/
public static final StringGuacamoleProperty LDAP_MEMBER_ATTRIBUTE = new StringGuacamoleProperty() {
public static final StringGuacamoleProperty LDAP_MEMBER_ATTRIBUTE =
new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-member-attribute"; }

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

@@ -17,7 +17,7 @@
* under the License.
*/
package org.apache.guacamole.auth.ldap;
package org.apache.guacamole.auth.ldap.conf;
import java.util.Arrays;
import java.util.List;

View File

@@ -20,17 +20,22 @@
package org.apache.guacamole.auth.ldap.connection;
import com.google.inject.Inject;
import com.novell.ldap.LDAPAttribute;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import java.util.Collections;
import java.util.Enumeration;
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.directory.ldap.client.api.LdapConnectionConfig;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
import org.apache.guacamole.auth.ldap.ConfigurationService;
import org.apache.guacamole.auth.ldap.EscapingService;
import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
@@ -53,13 +58,7 @@ public class ConnectionService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
/**
* Service for escaping parts of LDAP queries.
*/
@Inject
private EscapingService escapingService;
private static final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -100,17 +99,18 @@ public class ConnectionService {
* If an error occurs preventing retrieval of connections.
*/
public Map<String, Connection> getConnections(AuthenticatedUser user,
LDAPConnection ldapConnection) throws GuacamoleException {
LdapNetworkConnection ldapConnection) throws GuacamoleException {
// Do not return any connections if base DN is not specified
String configurationBaseDN = confService.getConfigurationBaseDN();
Dn configurationBaseDN = confService.getConfigurationBaseDN();
if (configurationBaseDN == null)
return Collections.<String, Connection>emptyMap();
try {
// Pull the current user DN from the LDAP connection
String userDN = ldapConnection.getAuthenticationDN();
LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
Dn userDN = new Dn(ldapConnectionConfig.getName());
// getConnections() will only be called after a connection has been
// authenticated (via non-anonymous bind), thus userDN cannot
@@ -119,46 +119,77 @@ public class ConnectionService {
// Get the search filter for finding connections accessible by the
// current user
String connectionSearchFilter = getConnectionSearchFilter(userDN, ldapConnection);
ExprNode connectionSearchFilter = getConnectionSearchFilter(userDN, ldapConnection);
// 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<LDAPEntry> results = queryService.search(ldapConnection, configurationBaseDN, connectionSearchFilter);
List<Entry> results = queryService.search(ldapConnection,
configurationBaseDN, connectionSearchFilter, 0);
// Return a map of all readable connections
return queryService.asMap(results, (entry) -> {
// Get common name (CN)
LDAPAttribute cn = entry.getAttribute("cn");
Attribute cn = entry.get("cn");
if (cn == null) {
logger.warn("guacConfigGroup is missing a cn.");
return null;
}
String cnName;
try {
cnName = cn.getString();
}
catch (LdapInvalidAttributeValueException e) {
logger.error("Invalid value for CN attribute: {}",
e.getMessage());
logger.debug("LDAP exception while getting CN attribute.", e);
return null;
}
// Get associated protocol
LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
Attribute protocol = entry.get("guacConfigProtocol");
if (protocol == null) {
logger.warn("guacConfigGroup \"{}\" is missing the "
+ "required \"guacConfigProtocol\" attribute.",
cn.getStringValue());
cnName);
return null;
}
// Set protocol
GuacamoleConfiguration config = new GuacamoleConfiguration();
config.setProtocol(protocol.getStringValue());
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 parameters, if any
LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
Attribute parameterAttribute = entry.get("guacConfigParameter");
if (parameterAttribute != null) {
// For each parameter
Enumeration<?> parameters = parameterAttribute.getStringValues();
while (parameters.hasMoreElements()) {
String parameter = (String) parameters.nextElement();
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('=');
@@ -177,8 +208,7 @@ public class ConnectionService {
}
// Store connection using cn for both identifier and name
String name = cn.getStringValue();
Connection connection = new SimpleConnection(name, name, config, true);
Connection connection = new SimpleConnection(cnName, cnName, config, true);
connection.setParentIdentifier(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP);
// Inject LDAP-specific tokens only if LDAP handled user
@@ -192,7 +222,7 @@ public class ConnectionService {
});
}
catch (LDAPException e) {
catch (LdapException e) {
throw new GuacamoleServerException("Error while querying for connections.", e);
}
@@ -213,40 +243,39 @@ public class ConnectionService {
* An LDAP search filter which queries all guacConfigGroup objects
* accessible by the user having the given DN.
*
* @throws LDAPException
* @throws LdapException
* If an error occurs preventing retrieval of user groups.
*
* @throws GuacamoleException
* If an error occurs retrieving the group base DN.
*/
private String getConnectionSearchFilter(String userDN,
LDAPConnection ldapConnection)
throws LDAPException, GuacamoleException {
private ExprNode getConnectionSearchFilter(Dn userDN,
LdapNetworkConnection ldapConnection)
throws LdapException, GuacamoleException {
// Create a search filter for the connection search
StringBuilder connectionSearchFilter = new StringBuilder();
AndNode searchFilter = new AndNode();
// Add the prefix to the search filter, prefix filter searches for guacConfigGroups with the userDN as the member attribute value
connectionSearchFilter.append("(&(objectClass=guacConfigGroup)");
connectionSearchFilter.append("(|(");
connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(
confService.getMemberAttribute()));
connectionSearchFilter.append("=");
connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(userDN));
connectionSearchFilter.append(")");
searchFilter.addNode(new EqualityNode("objectClass","guacConfigGroup"));
// Apply group filters
OrNode groupFilter = new OrNode();
groupFilter.addNode(new EqualityNode(confService.getMemberAttribute(),
userDN.toString()));
// Additionally filter by group membership if the current user is a
// member of any user groups
List<LDAPEntry> userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN);
List<Entry> userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN);
if (!userGroups.isEmpty()) {
for (LDAPEntry entry : userGroups)
connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")");
userGroups.forEach(entry ->
groupFilter.addNode(new EqualityNode("seeAlso",entry.getDn().toString()))
);
}
// Complete the search filter.
connectionSearchFilter.append("))");
searchFilter.addNode(groupFilter);
return connectionSearchFilter.toString();
return searchFilter;
}
}

View File

@@ -20,15 +20,21 @@
package org.apache.guacamole.auth.ldap.group;
import com.google.inject.Inject;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
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.guacamole.auth.ldap.ConfigurationService;
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.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.filter.PresenceNode;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
import org.apache.guacamole.net.auth.UserGroup;
@@ -45,7 +51,7 @@ public class UserGroupService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(UserGroupService.class);
private static final Logger logger = LoggerFactory.getLogger(UserGroupService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -72,17 +78,17 @@ public class UserGroupService {
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
private String getGroupSearchFilter() throws GuacamoleException {
private ExprNode getGroupSearchFilter() throws GuacamoleException {
// 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 (confService.getConfigurationBaseDN() != null)
return "(!(objectClass=guacConfigGroup))";
return new NotNode(new EqualityNode("objectClass","guacConfigGroup"));
// Read any object as a group if LDAP is not being used for connection
// storage (guacConfigGroup)
return "(objectClass=*)";
return new PresenceNode("objectClass");
}
@@ -102,17 +108,17 @@ public class UserGroupService {
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
public Map<String, UserGroup> getUserGroups(LDAPConnection ldapConnection)
public Map<String, UserGroup> getUserGroups(LdapNetworkConnection ldapConnection)
throws GuacamoleException {
// Do not return any user groups if base DN is not specified
String groupBaseDN = confService.getGroupBaseDN();
Dn groupBaseDN = confService.getGroupBaseDN();
if (groupBaseDN == null)
return Collections.emptyMap();
// Retrieve all visible user groups which are not guacConfigGroups
Collection<String> attributes = confService.getGroupNameAttributes();
List<LDAPEntry> results = queryService.search(
List<Entry> results = queryService.search(
ldapConnection,
groupBaseDN,
getGroupSearchFilter(),
@@ -125,13 +131,18 @@ public class UserGroupService {
return queryService.asMap(results, entry -> {
// Translate entry into UserGroup object having proper identifier
String name = queryService.getIdentifier(entry, attributes);
if (name != null)
return new SimpleUserGroup(name);
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());
+ "and will be ignored.", entry.getDn().toString());
return null;
});
@@ -157,11 +168,11 @@ public class UserGroupService {
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
public List<LDAPEntry> getParentUserGroupEntries(LDAPConnection ldapConnection,
String userDN) throws GuacamoleException {
public List<Entry> getParentUserGroupEntries(LdapNetworkConnection ldapConnection,
Dn userDN) throws GuacamoleException {
// Do not return any user groups if base DN is not specified
String groupBaseDN = confService.getGroupBaseDN();
Dn groupBaseDN = confService.getGroupBaseDN();
if (groupBaseDN == null)
return Collections.emptyList();
@@ -172,7 +183,7 @@ public class UserGroupService {
groupBaseDN,
getGroupSearchFilter(),
Collections.singleton(confService.getMemberAttribute()),
userDN
userDN.toString()
);
}
@@ -196,24 +207,31 @@ public class UserGroupService {
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
public Set<String> getParentUserGroupIdentifiers(LDAPConnection ldapConnection,
String userDN) throws GuacamoleException {
public Set<String> getParentUserGroupIdentifiers(LdapNetworkConnection ldapConnection,
Dn userDN) throws GuacamoleException {
Collection<String> attributes = confService.getGroupNameAttributes();
List<LDAPEntry> userGroups = getParentUserGroupEntries(ldapConnection, userDN);
List<Entry> userGroups = getParentUserGroupEntries(ldapConnection, userDN);
Set<String> identifiers = new HashSet<>(userGroups.size());
userGroups.forEach(entry -> {
// Determine unique identifier for user group
String name = queryService.getIdentifier(entry, attributes);
if (name != null)
identifiers.add(name);
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());
// 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);
}
});

View File

@@ -23,6 +23,7 @@ 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.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
@@ -56,6 +57,11 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser {
* available to this user.
*/
private Set<String> effectiveGroups;
/**
* The LDAP DN used to bind this user.
*/
private Dn bindDn;
/**
* Initializes this AuthenticatedUser with the given credentials,
@@ -71,14 +77,19 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser {
* @param effectiveGroups
* The unique identifiers of all user groups which affect the
* permissions available to this user.
*
* @param bindDn
* The LDAP DN used to bind this user.
*/
public void init(Credentials credentials, Map<String, String> tokens, Set<String> effectiveGroups) {
public void init(Credentials credentials, Map<String, String> tokens,
Set<String> effectiveGroups, Dn bindDn) {
this.credentials = credentials;
this.tokens = Collections.unmodifiableMap(tokens);
this.effectiveGroups = effectiveGroups;
this.bindDn = bindDn;
setIdentifier(credentials.getUsername());
}
/**
* Returns a Map of all name/value pairs that should be applied as
* parameter tokens when connections are established using this
@@ -92,6 +103,16 @@ public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser {
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;
}
@Override
public AuthenticationProvider getAuthenticationProvider() {

View File

@@ -20,8 +20,8 @@
package org.apache.guacamole.auth.ldap.user;
import com.google.inject.Inject;
import com.novell.ldap.LDAPConnection;
import java.util.Collections;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.auth.ldap.connection.ConnectionService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
@@ -39,8 +39,6 @@ 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An LDAP-specific implementation of UserContext which queries all Guacamole
@@ -48,11 +46,6 @@ import org.slf4j.LoggerFactory;
*/
public class LDAPUserContext extends AbstractUserContext {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(LDAPUserContext.class);
/**
* Service for retrieving Guacamole connections from the LDAP server.
*/
@@ -109,7 +102,7 @@ public class LDAPUserContext extends AbstractUserContext {
/**
* Initializes this UserContext using the provided AuthenticatedUser and
* LDAPConnection.
* LdapNetworkConnection.
*
* @param user
* The AuthenticatedUser representing the user that authenticated. This
@@ -124,7 +117,7 @@ public class LDAPUserContext extends AbstractUserContext {
* If associated data stored within the LDAP directory cannot be
* queried due to an error.
*/
public void init(AuthenticatedUser user, LDAPConnection ldapConnection)
public void init(AuthenticatedUser user, LdapNetworkConnection ldapConnection)
throws GuacamoleException {
// Query all accessible users

View File

@@ -20,16 +20,20 @@
package org.apache.guacamole.auth.ldap.user;
import com.google.inject.Inject;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.guacamole.auth.ldap.ConfigurationService;
import org.apache.guacamole.auth.ldap.EscapingService;
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.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.LDAPGuacamoleProperties;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.simple.SimpleUser;
@@ -45,13 +49,7 @@ public class UserService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* Service for escaping parts of LDAP queries.
*/
@Inject
private EscapingService escapingService;
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -81,12 +79,12 @@ public class UserService {
* @throws GuacamoleException
* If an error occurs preventing retrieval of users.
*/
public Map<String, User> getUsers(LDAPConnection ldapConnection)
public Map<String, User> getUsers(LdapNetworkConnection ldapConnection)
throws GuacamoleException {
// Retrieve all visible user objects
Collection<String> attributes = confService.getUsernameAttributes();
List<LDAPEntry> results = queryService.search(ldapConnection,
List<Entry> results = queryService.search(ldapConnection,
confService.getUserBaseDN(),
confService.getUserSearchFilter(),
attributes,
@@ -96,15 +94,21 @@ public class UserService {
return queryService.asMap(results, entry -> {
// Get username from record
String username = queryService.getIdentifier(entry, attributes);
if (username == null) {
logger.warn("User \"{}\" is missing a username attribute "
+ "and will be ignored.", entry.getDN());
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;
}
return new SimpleUser(username);
});
}
@@ -130,19 +134,19 @@ public class UserService {
* If an error occurs while querying the user DNs, or if the username
* attribute property cannot be parsed within guacamole.properties.
*/
public List<String> getUserDNs(LDAPConnection ldapConnection,
public List<Dn> getUserDNs(LdapNetworkConnection ldapConnection,
String username) throws GuacamoleException {
// Retrieve user objects having a matching username
List<LDAPEntry> results = queryService.search(ldapConnection,
List<Entry> results = queryService.search(ldapConnection,
confService.getUserBaseDN(),
confService.getUserSearchFilter(),
confService.getUsernameAttributes(),
username);
// Build list of all DNs for retrieved users
List<String> userDNs = new ArrayList<>(results.size());
results.forEach(entry -> userDNs.add(entry.getDN()));
List<Dn> userDNs = new ArrayList<>(results.size());
results.forEach(entry -> userDNs.add(entry.getDn()));
return userDNs;
@@ -164,7 +168,7 @@ public class UserService {
* If required properties are missing, and thus the user DN cannot be
* determined.
*/
public String deriveUserDN(String username)
public Dn deriveUserDN(String username)
throws GuacamoleException {
// Pull username attributes from properties
@@ -181,10 +185,13 @@ public class UserService {
}
// Derive user DN from base DN
return
escapingService.escapeDN(usernameAttributes.get(0))
+ "=" + escapingService.escapeDN(username)
+ "," + confService.getUserBaseDN();
try {
return new Dn(new Rdn(usernameAttributes.get(0), username),
confService.getUserBaseDN());
}
catch (LdapInvalidAttributeValueException | LdapInvalidDnException e) {
throw new GuacamoleServerException("Error trying to derive user DN.", e);
}
}