| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.guacamole.auth.ldap; |
| |
| import com.google.inject.Inject; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.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; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Service for executing queries against an LDAP directory intended to retrieve |
| * Guacamole-related objects. Referrals are automatically handled. Convenience |
| * functions are provided for generating the LDAP queries typically required |
| * for retrieving Guacamole objects, as well as for converting the results of a |
| * query into a {@link Map} of Guacamole objects. |
| */ |
| public class ObjectQueryService { |
| |
| /** |
| * Logger for this class. |
| */ |
| private static final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class); |
| |
| /** |
| * Service for connecting to LDAP directory. |
| */ |
| @Inject |
| private LDAPConnectionService ldapService; |
| |
| /** |
| * Returns the identifier of the object represented by the given LDAP |
| * entry. Multiple attributes may be declared as containing the identifier |
| * of the object when present on an LDAP entry. If multiple such attributes |
| * are present on the same LDAP entry, the value of the attribute with |
| * highest priority is used. If multiple copies of the same attribute are |
| * present on the same LDAPentry, the first value of that attribute is |
| * used. |
| * |
| * @param entry |
| * The entry representing the Guacamole object whose unique identifier |
| * should be determined. |
| * |
| * @param attributes |
| * A collection of all attributes which may be used to specify the |
| * unique identifier of the Guacamole object represented by an LDAP |
| * entry, in order of decreasing priority. |
| * |
| * @return |
| * The identifier of the object represented by the given LDAP entry, or |
| * null if no attributes declared as containing the identifier of the |
| * object are present on the entry. |
| * |
| * @throws LdapInvalidAttributeValueException |
| * If an error occurs retrieving the value of the identifier attribute. |
| */ |
| public String getIdentifier(Entry entry, Collection<String> attributes) |
| throws LdapInvalidAttributeValueException { |
| |
| // Retrieve the first value of the highest priority identifier attribute |
| for (String identifierAttribute : attributes) { |
| Attribute identifier = entry.get(identifierAttribute); |
| if (identifier != null) |
| return identifier.getString(); |
| } |
| |
| // No identifier attribute is present on the entry |
| return null; |
| |
| } |
| |
| /** |
| * Generates a properly-escaped LDAP query which finds all objects which |
| * match the given LDAP filter and which have at least one of the given |
| * attributes set to the specified value. |
| * |
| * @param filter |
| * The LDAP filter to apply to reduce the results of the query in |
| * addition to testing the values of the given attributes. |
| * |
| * @param attributes |
| * A collection of all attributes to test for equivalence to the given |
| * value, in order of decreasing priority. |
| * |
| * @param attributeValue |
| * The value that the resulting LDAP query should search for within the |
| * attributes of objects within the LDAP directory. If null, the |
| * resulting LDAP query will search for the presence of at least one of |
| * the given attributes on each object, regardless of the value of |
| * those attributes. |
| * |
| * @return |
| * An LDAP query which will search for arbitrary LDAP objects having at |
| * least one of the given attributes set to the specified value. |
| */ |
| public ExprNode generateQuery(ExprNode filter, |
| Collection<String> attributes, String attributeValue) { |
| |
| // Build LDAP query for objects having at least one attribute and with |
| // the given search filter |
| AndNode searchFilter = new AndNode(); |
| searchFilter.addNode(filter); |
| |
| // Include all attributes within OR clause if there are more than one |
| OrNode attributeFilter = new OrNode(); |
| |
| // Add equality comparison for each possible attribute |
| attributes.forEach(attribute -> |
| attributeFilter.addNode(new EqualityNode(attribute, attributeValue)) |
| ); |
| |
| searchFilter.addNode(attributeFilter); |
| |
| return searchFilter; |
| |
| } |
| |
| /** |
| * Executes an arbitrary LDAP query using the given connection, returning a |
| * list of all results. Only objects beneath the given base DN are |
| * included in the search. |
| * |
| * @param ldapConnection |
| * The current connection to the LDAP server, associated with the |
| * current user. |
| * |
| * @param baseDN |
| * The base DN to search using the given LDAP query. |
| * |
| * @param query |
| * The LDAP query to execute. |
| * |
| * @param searchHop |
| * The current level of referral depth for this search, used for |
| * limiting the maximum depth to which referrals can go. |
| * |
| * @return |
| * A list of all results accessible to the user currently bound under |
| * the given LDAP connection. |
| * |
| * @throws GuacamoleException |
| * If an error occurs executing the query, or if configuration |
| * information required to execute the query cannot be read from |
| * guacamole.properties. |
| */ |
| public List<Entry> search(LdapNetworkConnection ldapConnection, |
| Dn baseDN, ExprNode query, int searchHop) throws GuacamoleException { |
| |
| logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query); |
| |
| LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig(); |
| |
| // Search within subtree of given base DN |
| SearchRequest request = ldapService.getSearchRequest(baseDN, |
| query); |
| |
| // Produce list of all entries in the search result, automatically |
| // following referrals if configured to do so |
| List<Entry> entries = new ArrayList<>(); |
| |
| try (SearchCursor results = ldapConnection.search(request)) { |
| while (results.next()) { |
| |
| if (results.isEntry()) { |
| entries.add(results.getEntry()); |
| } |
| 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)); |
| } |
| |
| } |
| |
| } |
| |
| return entries; |
| |
| } |
| catch (CursorException | IOException | LdapException e) { |
| throw new GuacamoleServerException("Unable to query list of " |
| + "objects from LDAP directory.", e); |
| } |
| |
| } |
| |
| /** |
| * Executes the query which would be returned by generateQuery() using the |
| * given connection, returning a list of all results. Only objects beneath |
| * the given base DN are included in the search. |
| * |
| * @param ldapConnection |
| * The current connection to the LDAP server, associated with the |
| * current user. |
| * |
| * @param baseDN |
| * The base DN to search using the given LDAP query. |
| * |
| * @param filter |
| * The LDAP filter to apply to reduce the results of the query in |
| * addition to testing the values of the given attributes. |
| * |
| * @param attributes |
| * A collection of all attributes to test for equivalence to the given |
| * value, in order of decreasing priority. |
| * |
| * @param attributeValue |
| * The value that should be searched search for within the attributes |
| * of objects within the LDAP directory. If null, the search will test |
| * only for the presence of at least one of the given attributes on |
| * each object, regardless of the value of those attributes. |
| * |
| * @return |
| * A list of all results accessible to the user currently bound under |
| * the given LDAP connection. |
| * |
| * @throws GuacamoleException |
| * If an error occurs executing the query, or if configuration |
| * information required to execute the query cannot be read from |
| * guacamole.properties. |
| */ |
| public List<Entry> search(LdapNetworkConnection ldapConnection, Dn baseDN, |
| ExprNode filter, Collection<String> attributes, String attributeValue) |
| throws GuacamoleException { |
| ExprNode query = generateQuery(filter, attributes, attributeValue); |
| return search(ldapConnection, baseDN, query, 0); |
| } |
| |
| /** |
| * Converts a given list of LDAP entries to a {@link Map} of Guacamole |
| * objects stored by their identifiers. |
| * |
| * @param <ObjectType> |
| * The type of object to store within the {@link Map}. |
| * |
| * @param entries |
| * A list of LDAP entries to convert to Guacamole objects. |
| * |
| * @param mapper |
| * A mapping function which converts a given LDAP entry to its |
| * corresponding Guacamole object. If the LDAP entry cannot be |
| * converted, null should be returned. |
| * |
| * @return |
| * A new {@link Map} containing Guacamole object versions of each of |
| * the given LDAP entries, where each object is stored within the |
| * {@link Map} under its corresponding identifier. |
| */ |
| public <ObjectType extends Identifiable> Map<String, ObjectType> |
| asMap(List<Entry> entries, Function<Entry, ObjectType> mapper) { |
| |
| // Convert each entry to the corresponding Guacamole API object |
| Map<String, ObjectType> objects = new HashMap<>(entries.size()); |
| for (Entry entry : entries) { |
| |
| ObjectType object = mapper.apply(entry); |
| if (object == null) { |
| logger.debug("Ignoring object \"{}\".", entry.getDn().toString()); |
| continue; |
| } |
| |
| // Attempt to add object to map, warning if the object appears |
| // to be a duplicate |
| String identifier = object.getIdentifier(); |
| if (objects.putIfAbsent(identifier, object) != null) |
| logger.warn("Multiple objects ambiguously map to the " |
| + "same identifier (\"{}\"). Ignoring \"{}\" as " |
| + "a duplicate.", identifier, entry.getDn().toString()); |
| |
| } |
| |
| return objects; |
| |
| } |
| |
| } |