All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pl.edu.icm.unity.ldap.client.LdapClient Maven / Gradle / Ivy

There is a newer version: 4.0.3
Show newest version
/*
 * Copyright (c) 2013 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE.txt file for licensing information.
 */
package pl.edu.icm.unity.ldap.client;

import static pl.edu.icm.unity.ldap.client.LdapUtils.nonEmpty;

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.net.ssl.SSLContext;

import org.apache.logging.log4j.Logger;

import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.FailoverServerSet;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ReadOnlySearchRequest;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;

import eu.emi.security.authn.x509.X509CertChainValidator;
import eu.unicore.security.canl.SSLContextCreator;
import eu.unicore.util.httpclient.ServerHostnameCheckingMode;
import pl.edu.icm.unity.base.utils.Log;
import pl.edu.icm.unity.engine.api.authn.remote.RemoteAttribute;
import pl.edu.icm.unity.engine.api.authn.remote.RemoteGroupMembership;
import pl.edu.icm.unity.engine.api.authn.remote.RemoteIdentity;
import pl.edu.icm.unity.engine.api.authn.remote.RemotelyAuthenticatedInput;
import pl.edu.icm.unity.ldap.client.config.GroupSpecification;
import pl.edu.icm.unity.ldap.client.config.LdapClientConfiguration;
import pl.edu.icm.unity.ldap.client.config.SearchSpecification;
import pl.edu.icm.unity.ldap.client.config.LdapProperties.BindAs;
import pl.edu.icm.unity.ldap.client.config.common.LDAPConnectionProperties.ConnectionMode;
import pl.edu.icm.unity.stdext.identity.X500Identity;

/**
 * LDAP v3 client code. Immutable -> thread safe.
 * 

* Depending on configuration, it's possible to bind as normal user, or as a privileged (system) user * who is allowed to search a whole (sub-)tree *

* When binding as user, the code first binds with the provided username and password credentials. * If this succeeds then (depending on configuration) user's attributes are retrieved and/or user's * groups are assembled. *

* When binding as system user, the code first binds with the provided system username and password credentials. * The requested user is then searched. If found, the code checks the user password by attempting to * bind as the user. If this succeeds, the code again binds as system user, and proceeds to retrieve user * attributes as above. *

* The attributes searching is pretty straightforward. The most of the code in this class is responsible for * flexible group retrieval. Both 'memberOf' style and 'member' means of expressing group membership are supported, * with some additional options. Most notably it is possible to use a full DN of the group or its attribute * as the group name. * * @author K. Benedyczak */ public class LdapClient { private static final Logger log = Log.getLogger(Log.U_SERVER_LDAP, LdapClient.class); private String idpName; private final LdapGroupHelper groupHelper; public LdapClient(String idpName) { this.idpName = idpName; this.groupHelper = new LdapGroupHelper(); } public LdapClient() { this.groupHelper = new LdapGroupHelper(); } /** * Performs authentication by binding and searches for all configured attributes. */ public RemotelyAuthenticatedInput bindAndSearch(String userOrig, String password, LdapClientConfiguration configuration) throws LDAPException, LdapAuthenticationException, KeyManagementException, NoSuchAlgorithmException { LDAPConnection connection = createConnection(configuration); String user = LdapUtils.extractUsername(userOrig, configuration.getUserExtractPattern()); String dn = establishUserDN(user, configuration, connection); log.info("Established user's DN is: " + dn); bindAsUser(connection, dn, password, configuration); if (configuration.isBindOnly()) { RemotelyAuthenticatedInput ret = new RemotelyAuthenticatedInput(idpName); ret.addIdentity(new RemoteIdentity(dn, X500Identity.ID)); return ret; } if (configuration.getBindAs() == BindAs.system) bindAsSystem(connection, configuration); SearchResultEntry entry = findBaseEntry(configuration, dn, connection); RemotelyAuthenticatedInput ret = assembleBaseResult(entry); findGroupsMembership(connection, entry, configuration, ret.getGroups()); performAdditionalQueries(connection, configuration, user, ret); ret.setRawAttributes(ret.getAttributes()); connection.close(); return ret; } /** * Resolves information about a given user, with all the features, but without binding as the user * (so the user must be authenticated with other means). This works only when configured to bind as system. * @param user * @param configuration * @return * @throws LDAPException * @throws LdapAuthenticationException * @throws KeyManagementException * @throws NoSuchAlgorithmException */ public RemotelyAuthenticatedInput search(String userOrig, LdapClientConfiguration configuration) throws LDAPException, LdapAuthenticationException, KeyManagementException, NoSuchAlgorithmException { if (configuration.getBindAs() == BindAs.user) { log.error("LDAP verification of externaly verified credentials (as TLS verified certificates)" + " can be only performed when the LDAP subsystem is configured to bind " + "with a system credential"); throw new LdapAuthenticationException("Can't authenticate"); } String user = LdapUtils.extractUsername(userOrig, configuration.getUserExtractPattern()); LDAPConnection connection = createConnection(configuration); String dn = establishUserDN(user, configuration, connection); log.info("Established user's DN is: " + dn); if (configuration.isBindOnly()) { RemotelyAuthenticatedInput ret = new RemotelyAuthenticatedInput(idpName); ret.addIdentity(new RemoteIdentity(dn, X500Identity.ID)); return ret; } if (configuration.getBindAs() == BindAs.system) bindAsSystem(connection, configuration); SearchResultEntry entry = findBaseEntry(configuration, dn, connection); RemotelyAuthenticatedInput ret = assembleBaseResult(entry); findGroupsMembership(connection, entry, configuration, ret.getGroups()); performAdditionalQueries(connection, configuration, user, ret); ret.setRawAttributes(ret.getAttributes()); connection.close(); return ret; } public Optional searchAttribute(String userOrig, String attributeName, LdapClientConfiguration configuration) throws LDAPException, LdapAuthenticationException, KeyManagementException, NoSuchAlgorithmException { if (configuration.getBindAs() != BindAs.system) { log.error("Bind with system credential required"); throw new LdapAuthenticationException("Can't authenticate"); } String user = LdapUtils.extractUsername(userOrig, configuration.getUserExtractPattern()); LDAPConnection connection = createConnection(configuration); String dn = establishUserDN(user, configuration, connection); log.info("Established user's DN is: " + dn); bindAsSystem(connection, configuration); SearchResultEntry entry = findBaseEntry(configuration, dn, connection); connection.close(); Attribute attribute = entry.getAttribute(attributeName); if (attribute != null) { return Optional.ofNullable(attribute.getValue()); } return Optional.empty(); } /** * Returns DN of the user. Depending on configuration the user's DN can be simply formed from a * configured template or can be discovered with a custom search run as admin user. * @param username * @param configuration * @param connection * @return * @throws LDAPException * @throws LdapAuthenticationException */ private String establishUserDN(String username, LdapClientConfiguration configuration, LDAPConnection connection) throws LDAPException, LdapAuthenticationException { SearchSpecification searchForUser = configuration.getSearchForUserSpec(); if (searchForUser == null) return configuration.getBindDN(username); bindAsSystem(connection, configuration); int timeLimit = configuration.getSearchTimeLimit(); int sizeLimit = configuration.getResultEntriesLimit(); DereferencePolicy derefPolicy = configuration.getDereferencePolicy(); SearchResult result = performSearch(connection, searchForUser, username, timeLimit, sizeLimit, derefPolicy); if (result.getEntryCount() == 0) { log.debug("Search for the user DN returned no results"); throw new LdapAuthenticationException("User was not found"); } else if (result.getEntryCount() > 1) { log.debug("Search for the user DN returned moe than one results"); throw new LdapAuthenticationException("Too many users found"); } else { return result.getSearchEntries().get(0).getDN(); } } /** * Creates an ladp connection and secures it. Failover settings from configuration are taken into account. * @param configuration * @return * @throws KeyManagementException * @throws NoSuchAlgorithmException * @throws LDAPException */ private LDAPConnection createConnection(LdapClientConfiguration configuration) throws KeyManagementException, NoSuchAlgorithmException, LDAPException { LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions(); connectionOptions.setConnectTimeoutMillis(configuration.getSocketTimeout()); connectionOptions.setFollowReferrals(configuration.isFollowReferral()); connectionOptions.setReferralHopLimit(configuration.getFollowReferrals()); connectionOptions.setResponseTimeoutMillis(configuration.getSocketReadTimeout()); FailoverServerSet failoverSet; if (configuration.getConnectionMode() == ConnectionMode.SSL) { X509CertChainValidator validator = configuration.getConnectionValidator(); ServerHostnameCheckingMode certificateCheckingMode = configuration.isTrustAllCerts() ? ServerHostnameCheckingMode.NONE : ServerHostnameCheckingMode.FAIL; SSLContext ctx = SSLContextCreator.createSSLContext(null, validator, "TLS", "LDAP client", log, certificateCheckingMode); log.debug("Will connect over TLS to: {}:{}", configuration.getServersAddresses(), configuration.getPorts()); failoverSet = new FailoverServerSet(configuration.getServersAddresses(), configuration.getPorts(), ctx.getSocketFactory(), connectionOptions); } else { log.debug("Will connect to: {}:{}", configuration.getServersAddresses(), configuration.getPorts()); failoverSet = new FailoverServerSet(configuration.getServersAddresses(), configuration.getPorts(), connectionOptions); } LDAPConnection connection = failoverSet.getConnection(); log.debug("Established connection to LDAP server: {}:{} (secured: {})", connection.getConnectedAddress(), connection.getConnectedPort(), connection.getSSLSession() != null); if (configuration.getConnectionMode() == ConnectionMode.startTLS) { X509CertChainValidator validator = configuration.getConnectionValidator(); SSLContext ctx = SSLContextCreator.createSSLContext(null, validator, "TLSv1.2", "LDAP client", log, ServerHostnameCheckingMode.FAIL); ExtendedResult extendedResult = connection.processExtendedOperation( new StartTLSExtendedRequest(ctx)); if (extendedResult.getResultCode() != ResultCode.SUCCESS) { connection.close(); throw new LDAPException(extendedResult.getResultCode(), "Unable to esablish " + "a secure TLS connection to the LDAP server: " + extendedResult.toString()); } log.debug("Connection upgraded to TLS"); } return connection; } private void bindAsUser(LDAPConnection connection, String dn, String password, LdapClientConfiguration configuration) throws LdapAuthenticationException, LDAPException { try { connection.bind(dn, password); } catch (LDAPException e) { if (ResultCode.INVALID_CREDENTIALS.equals(e.getResultCode())) { log.debug("LDAP bind as user " + dn + " was not successful - invalid password"); throw new LdapAuthenticationException("Wrong username or credentials", e); } else throw e; } log.info("LDAP bind as user " + dn + " was successful"); } private void bindAsSystem(LDAPConnection connection, LdapClientConfiguration configuration) throws LdapAuthenticationException, LDAPException { String systemDN = configuration.getSystemDN(); String systemPassword = configuration.getSystemPassword(); try { connection.bind(systemDN, systemPassword); } catch (LDAPException e) { if (ResultCode.INVALID_CREDENTIALS.equals(e.getResultCode())) throw new LdapAuthenticationException("Wrong username or credentials of the " + "system LDAP client " + "(system, not the ones provided by the user)", e); else throw e; } log.debug("LDAP bind as system user was successful"); } private SearchResultEntry findBaseEntry(LdapClientConfiguration configuration, String dn, LDAPConnection connection) throws LdapAuthenticationException, LDAPException { String[] queriedAttributes = configuration.getRetrievalLdapAttributes().stream().toArray(String[]::new); SearchScope searchScope = configuration.getSearchScope(); int timeLimit = configuration.getSearchTimeLimit(); int sizeLimit = configuration.getResultEntriesLimit(); DereferencePolicy derefPolicy = configuration.getDereferencePolicy(); Filter validUsersFilter = configuration.getParsedValidUserFilter(); ReadOnlySearchRequest searchRequest = new SearchRequest(dn, searchScope, derefPolicy, sizeLimit, timeLimit, false, validUsersFilter, queriedAttributes); SearchResult result = connection.search(searchRequest); SearchResultEntry entry = result.getSearchEntry(dn); if (entry == null) throw new LdapAuthenticationException("User is not matching the valid users filter"); return entry; } private RemotelyAuthenticatedInput assembleBaseResult(SearchResultEntry entry) { RemotelyAuthenticatedInput ret = new RemotelyAuthenticatedInput(idpName); for (Attribute a: entry.getAttributes()) { ret.addAttribute(new RemoteAttribute(a.getName(), (Object[])a.getValues())); } ret.setRawAttributes(ret.getAttributes()); ret.addIdentity(new RemoteIdentity(entry.getDN(), X500Identity.ID)); return ret; } private void findGroupsMembership(LDAPConnection connection, SearchResultEntry userEntry, LdapClientConfiguration configuration, Map ret) throws LDAPException { if (nonEmpty(configuration.getMemberOfAttribute())) groupHelper.findMemberOfGroups(ret, userEntry, configuration); if (nonEmpty(configuration.getGroupsBaseName())) searchGroupsForMember(connection, ret, userEntry, configuration); } /** * Complex variant: groups needs to be retrieved and searched for the user being a member. * @param connection * @param ret * @param userEntry * @param configuration * @throws LDAPException */ private void searchGroupsForMember(LDAPConnection connection, Map ret, SearchResultEntry userEntry, LdapClientConfiguration configuration) throws LDAPException { String base = configuration.getGroupsBaseName(); List gss = configuration.getGroupSpecifications(); Map gsByObjectClass = new HashMap<>(gss.size()); Set attributes = new HashSet<>(); attributes.add("objectClass"); String searchFilter = groupHelper.buildGroupFilter(userEntry, gss, configuration.isDelegateGroupFiltering()); for (GroupSpecification gs: gss) { String oc = gs.getObjectClass(); gsByObjectClass.put(oc, gs); if (nonEmpty(gs.getMemberAttribute())) attributes.add(gs.getMemberAttribute()); if (nonEmpty(gs.getGroupNameAttribute())) attributes.add(gs.getGroupNameAttribute()); } Filter filter; try { filter = Filter.create(searchFilter); } catch (LDAPException e) { throw new LDAPException(e.getResultCode(), "Specification of group object class is wrong. Unable to create a filter, " + "which was: " + searchFilter, e); } if (log.isDebugEnabled()) log.debug("Will search groups, from base "+ base + " with filter " + searchFilter + " collecting attributes " + attributes); ReadOnlySearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, configuration.getDereferencePolicy(), configuration.getResultEntriesLimit(), configuration.getSearchTimeLimit(), false, filter, attributes.toArray(new String[attributes.size()])); SearchResult result = connection.search(searchRequest); for (SearchResultEntry groupEntry: result.getSearchEntries()) { String[] classes = groupEntry.getObjectClassValues(); for (String clazz: classes) { GroupSpecification gs = gsByObjectClass.get(clazz); if (gs != null) { groupHelper.findMemberInGroup(ret, userEntry, groupEntry, gs); break; } } } } private void performAdditionalQueries(LDAPConnection connection, LdapClientConfiguration configuration, String user, RemotelyAuthenticatedInput principalData) throws LDAPException { int timeLimit = configuration.getSearchTimeLimit(); int sizeLimit = configuration.getResultEntriesLimit(); DereferencePolicy derefPolicy = configuration.getDereferencePolicy(); List searchSpecs = configuration.getSearchSpecifications(); for (SearchSpecification searchSpec: searchSpecs) { SearchResult result = performSearch(connection, searchSpec, user, timeLimit, sizeLimit, derefPolicy); consolidateAttributes(result, principalData); } } private SearchResult performSearch(LDAPConnection connection, SearchSpecification searchSpec, String username, int timeLimit, int sizeLimit, DereferencePolicy derefPolicy) throws LDAPException { String[] queriedAttributes = searchSpec.getSplitedAttributes(); Filter validUsersFilter = searchSpec.getFilter(username); String base = searchSpec.getBaseDN(username); SearchScope scope = searchSpec.getScope().getInternalScope(); log.debug("Performing LDAP search filter: [" + validUsersFilter + "] base: [" + base + "] scope: [" + scope +"]"); ReadOnlySearchRequest searchRequest = new SearchRequest(base, scope, derefPolicy, sizeLimit, timeLimit, false, validUsersFilter, queriedAttributes); return connection.search(searchRequest); } private void consolidateAttributes(SearchResult result, RemotelyAuthenticatedInput principalData) { Map> attributes = new HashMap>(); for (SearchResultEntry entry: result.getSearchEntries()) { for (Attribute a: entry.getAttributes()) { Set curValues = attributes.get(a.getName()); if (curValues == null) { curValues = new LinkedHashSet(); attributes.put(a.getName(), curValues); } Collections.addAll(curValues, a.getValues()); } } for (Map.Entry> e: attributes.entrySet()) { principalData.addAttribute(new RemoteAttribute(e.getKey(), e.getValue().toArray())); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy