| /* |
| * 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.jackrabbit.oak.security.authentication.ldap.impl; |
| |
| import java.io.IOException; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| |
| import javax.jcr.Credentials; |
| import javax.jcr.SimpleCredentials; |
| import javax.net.ssl.SSLContext; |
| import javax.security.auth.login.LoginException; |
| |
| import org.apache.commons.pool.impl.GenericObjectPool; |
| import org.apache.directory.api.ldap.codec.controls.search.pagedSearch.PagedResultsDecorator; |
| import org.apache.directory.api.ldap.model.constants.SchemaConstants; |
| 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.entry.Value; |
| import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException; |
| import org.apache.directory.api.ldap.model.exception.LdapException; |
| import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException; |
| import org.apache.directory.api.ldap.model.message.Response; |
| 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.SearchResultDone; |
| import org.apache.directory.api.ldap.model.message.SearchResultEntry; |
| import org.apache.directory.api.ldap.model.message.SearchScope; |
| import org.apache.directory.api.ldap.model.message.controls.PagedResults; |
| 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.AbstractPoolableLdapConnectionFactory; |
| import org.apache.directory.ldap.client.api.DefaultLdapConnectionValidator; |
| import org.apache.directory.ldap.client.api.LdapConnection; |
| import org.apache.directory.ldap.client.api.LdapConnectionConfig; |
| import org.apache.directory.ldap.client.api.LdapConnectionPool; |
| import org.apache.directory.ldap.client.api.LookupLdapConnectionValidator; |
| import org.apache.directory.ldap.client.api.NoVerificationTrustManager; |
| import org.apache.directory.ldap.client.api.ValidatingPoolableLdapConnectionFactory; |
| import org.apache.felix.scr.annotations.Activate; |
| import org.apache.felix.scr.annotations.Component; |
| import org.apache.felix.scr.annotations.ConfigurationPolicy; |
| import org.apache.felix.scr.annotations.Deactivate; |
| import org.apache.felix.scr.annotations.Service; |
| import org.apache.jackrabbit.commons.iterator.AbstractLazyIterator; |
| import org.apache.jackrabbit.oak.commons.DebugTimer; |
| import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalGroup; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityException; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.PrincipalNameResolver; |
| import org.apache.jackrabbit.util.Text; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * {@code LdapIdentityProvider} implements an external identity provider that reads users and groups from an ldap |
| * source. |
| * |
| * Please refer to {@link LdapProviderConfig} for configuration options. |
| */ |
| @Component( |
| // note that the metatype information is generated from LdapProviderConfig |
| policy = ConfigurationPolicy.REQUIRE |
| ) |
| @Service |
| public class LdapIdentityProvider implements ExternalIdentityProvider, PrincipalNameResolver { |
| |
| /** |
| * default logger |
| */ |
| private static final Logger log = LoggerFactory.getLogger(LdapIdentityProvider.class); |
| |
| /** |
| * internal configuration |
| */ |
| private LdapProviderConfig config; |
| |
| /** |
| * the connection pool with connections authenticated with the bind DN |
| */ |
| private LdapConnectionPool adminPool; |
| |
| /** |
| * admin connection factory |
| */ |
| private AbstractPoolableLdapConnectionFactory adminConnectionFactory; |
| |
| /** |
| * the connection pool with unbound connections |
| */ |
| private UnboundLdapConnectionPool userPool; |
| |
| /** |
| * user connection factory |
| */ |
| private PoolableUnboundConnectionFactory userConnectionFactory; |
| |
| /** |
| * SSL protocols (initialized on init) |
| */ |
| private String[] enabledSSLProtocols; |
| |
| /** |
| * Default constructor for OSGi |
| */ |
| @SuppressWarnings("UnusedDeclaration") |
| public LdapIdentityProvider() { |
| } |
| |
| /** |
| * Constructor for non-OSGi cases. |
| * @param config the configuration |
| */ |
| public LdapIdentityProvider(@NotNull LdapProviderConfig config) { |
| this.config = config; |
| init(); |
| } |
| |
| //----------------------------------------------------< SCR integration >--- |
| @SuppressWarnings("UnusedDeclaration") |
| @Activate |
| private void activate(Map<String, Object> properties) { |
| ConfigurationParameters cfg = ConfigurationParameters.of(properties); |
| config = LdapProviderConfig.of(cfg); |
| init(); |
| } |
| |
| @SuppressWarnings("UnusedDeclaration") |
| @Deactivate |
| private void deactivate() { |
| close(); |
| } |
| |
| /** |
| * Closes this provider and releases the internal pool. This should be called by Non-OSGi users of this provider. |
| */ |
| public void close() { |
| if (adminPool != null) { |
| try { |
| adminPool.close(); |
| } catch (Exception e) { |
| log.warn("Error while closing LDAP connection pool", e); |
| } |
| adminPool = null; |
| } |
| if (userPool != null) { |
| try { |
| userPool.close(); |
| } catch (Exception e) { |
| log.warn("Error while closing LDAP connection pool", e); |
| } |
| userPool = null; |
| } |
| } |
| |
| //----------------------------------------------< PrincipalNameResolver >--- |
| @NotNull |
| @Override |
| public String fromExternalIdentityRef(@NotNull ExternalIdentityRef externalIdentityRef) throws ExternalIdentityException { |
| if (!isMyRef(externalIdentityRef)) { |
| throw new ExternalIdentityException("Foreign IDP " + externalIdentityRef.getString()); |
| } |
| return externalIdentityRef.getId(); |
| } |
| |
| //-------------------------------------------< ExternalIdentityProvider >--- |
| @NotNull |
| @Override |
| public String getName() { |
| return config.getName(); |
| } |
| |
| @Override |
| public ExternalIdentity getIdentity(@NotNull ExternalIdentityRef ref) throws ExternalIdentityException { |
| if (!isMyRef(ref)) { |
| return null; |
| } |
| |
| LdapConnection connection = connect(); |
| try { |
| String userIdAttr = config.getUserConfig().getIdAttribute(); |
| String groupIdAttr = config.getGroupConfig().getIdAttribute(); |
| String[] ca = config.getCustomAttributes(); |
| Entry entry; |
| if (ca.length == 0) { |
| entry = connection.lookup(ref.getId(), SchemaConstants.ALL_USER_ATTRIBUTES); |
| } |
| else { |
| List<String> attributes = new ArrayList<>(Arrays.asList(ca)); |
| attributes.add("objectClass"); |
| attributes.add(userIdAttr); |
| attributes.add(groupIdAttr); |
| String[] attributeArray = new String[attributes.size()]; |
| attributes.toArray(attributeArray); |
| entry = connection.lookup(ref.getId(), attributeArray); |
| } |
| if (entry == null) { |
| return null; |
| } else if (entry.hasObjectClass(config.getUserConfig().getObjectClasses())) { |
| return createUser(entry, null); |
| } else if (entry.hasObjectClass(config.getGroupConfig().getObjectClasses())) { |
| return createGroup(entry, null); |
| } else { |
| log.warn("referenced identity is neither user or group: {}", ref.getString()); |
| return null; |
| } |
| } catch (LdapException e) { |
| throw lookupFailedException(e, null); |
| } finally { |
| disconnect(connection); |
| } |
| } |
| |
| @Override |
| public ExternalUser getUser(@NotNull String userId) throws ExternalIdentityException { |
| DebugTimer timer = new DebugTimer(); |
| LdapConnection connection = connect(); |
| timer.mark("connect"); |
| try { |
| Entry entry = getEntry(connection, config.getUserConfig(), userId, config.getCustomAttributes()); |
| timer.mark("lookup"); |
| if (log.isDebugEnabled()) { |
| log.debug("getUser({}) {}", userId, timer.getString()); |
| } |
| if (entry != null) { |
| return createUser(entry, userId); |
| } else { |
| return null; |
| } |
| } catch (LdapException | CursorException e) { |
| throw lookupFailedException(e, timer); |
| } finally { |
| disconnect(connection); |
| } |
| } |
| |
| @Override |
| public ExternalGroup getGroup(@NotNull String name) throws ExternalIdentityException { |
| DebugTimer timer = new DebugTimer(); |
| LdapConnection connection = connect(); |
| timer.mark("connect"); |
| try { |
| Entry entry = getEntry(connection, config.getGroupConfig(), name, config.getCustomAttributes()); |
| timer.mark("lookup"); |
| if (log.isDebugEnabled()) { |
| log.debug("getGroup({}) {}", name, timer.getString()); |
| } |
| if (entry != null) { |
| return createGroup(entry, name); |
| } else { |
| return null; |
| } |
| } catch (LdapException | CursorException e) { |
| throw lookupFailedException(e, timer); |
| } finally { |
| disconnect(connection); |
| } |
| } |
| |
| @NotNull |
| @Override |
| public Iterator<ExternalUser> listUsers() throws ExternalIdentityException { |
| try { |
| final Iterator<Entry> iter = getEntryIterator(config.getUserConfig()); |
| return new AbstractLazyIterator<ExternalUser>() { |
| |
| @Override |
| protected ExternalUser getNext() { |
| while (iter.hasNext()) { |
| try { |
| return createUser(iter.next(), null); |
| } catch (LdapInvalidAttributeValueException e) { |
| log.warn("Error while creating external user object", e); |
| } |
| } |
| return null; |
| } |
| }; |
| } catch (LdapException | CursorException e) { |
| throw lookupFailedException(e, null); |
| } |
| } |
| |
| @NotNull |
| @Override |
| public Iterator<ExternalGroup> listGroups() throws ExternalIdentityException { |
| try { |
| final Iterator<Entry> iter = getEntryIterator(config.getGroupConfig()); |
| return new AbstractLazyIterator<ExternalGroup>() { |
| |
| @Override |
| protected ExternalGroup getNext() { |
| while (iter.hasNext()) { |
| try { |
| return createGroup(iter.next(), null); |
| } catch (LdapInvalidAttributeValueException e) { |
| log.warn("Error while creating external user object", e); |
| } |
| } |
| return null; |
| } |
| }; |
| } catch (LdapException | CursorException e) { |
| throw lookupFailedException(e, null); |
| } |
| } |
| |
| @Override |
| public ExternalUser authenticate(@NotNull Credentials credentials) throws ExternalIdentityException, LoginException { |
| if (!(credentials instanceof SimpleCredentials)) { |
| log.debug("LDAP IDP can only authenticate SimpleCredentials."); |
| return null; |
| } |
| final SimpleCredentials creds = (SimpleCredentials) credentials; |
| final LdapUser user = (LdapUser)getUser(creds.getUserID()); |
| if (user != null) { |
| // OAK-2078: check for non-empty passwords to avoid anonymous bind on weakly configured servers |
| // see http://tools.ietf.org/html/rfc4513#section-5.1.1 for details. |
| if (creds.getPassword().length == 0) { |
| throw new LoginException("Refusing to authenticate against LDAP server: Empty passwords not allowed."); |
| } |
| |
| // authenticate |
| LdapConnection connection = null; |
| try { |
| DebugTimer timer = new DebugTimer(); |
| if (userPool == null) { |
| connection = userConnectionFactory.makeObject(); |
| } else { |
| connection = userPool.getConnection(); |
| } |
| timer.mark("connect"); |
| connection.bind(user.getEntry().getDn(), new String(creds.getPassword())); |
| //connection.bind(user.getExternalId().getId(), new String(creds.getPassword())); |
| timer.mark("bind"); |
| if (log.isDebugEnabled()) { |
| log.debug("authenticate({}) {}", user.getId(), timer.getString()); |
| } |
| } catch (LdapAuthenticationException e) { |
| throw new LoginException("Unable to authenticate against LDAP server: " + e.getMessage()); |
| } catch (Exception e) { |
| throw new ExternalIdentityException("Error while binding user credentials", e); |
| } finally { |
| if (connection != null) { |
| try { |
| if (userPool == null) { |
| userConnectionFactory.destroyObject(connection); |
| } else { |
| userPool.releaseConnection(connection); |
| } |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| } |
| } |
| return user; |
| } |
| |
| //-----------------------------------------------------------< internal >--- |
| |
| /** |
| * Collects the declared (direct) groups of an identity |
| * @param ref reference to the identity |
| * @return map of identities where the key is the DN of the LDAP entity |
| */ |
| Map<String, ExternalIdentityRef> getDeclaredGroupRefs(ExternalIdentityRef ref, String dn) throws ExternalIdentityException { |
| if (!isMyRef(ref)) { |
| return Collections.emptyMap(); |
| } |
| String searchFilter = config.getMemberOfSearchFilter(dn); |
| |
| LdapConnection connection = null; |
| SearchCursor searchCursor = null; |
| try { |
| // Create the SearchRequest object |
| SearchRequest req = new SearchRequestImpl(); |
| req.setScope(SearchScope.SUBTREE); |
| String idAttribute = config.getGroupConfig().getIdAttribute(); |
| req.addAttributes(idAttribute == null? SchemaConstants.NO_ATTRIBUTE : idAttribute); |
| req.setTimeLimit((int) config.getSearchTimeout()); |
| req.setBase(new Dn(config.getGroupConfig().getBaseDN())); |
| req.setFilter(searchFilter); |
| |
| if (log.isDebugEnabled()) { |
| log.debug("getDeclaredGroupRefs: using SearchRequest {}.", req); |
| } |
| |
| Map<String, ExternalIdentityRef> groups = new HashMap<String, ExternalIdentityRef>(); |
| DebugTimer timer = new DebugTimer(); |
| connection = connect(); |
| timer.mark("connect"); |
| |
| searchCursor = connection.search(req); |
| timer.mark("search"); |
| while (searchCursor.next()) { |
| Response response = searchCursor.get(); |
| if (response instanceof SearchResultEntry) { |
| Entry resultEntry = ((SearchResultEntry) response).getEntry(); |
| ExternalIdentityRef groupRef = new ExternalIdentityRef(resultEntry.getDn().toString(), this.getName()); |
| groups.put(groupRef.getId(), groupRef); |
| } |
| } |
| timer.mark("iterate"); |
| if (log.isDebugEnabled()) { |
| log.debug("getDeclaredGroupRefs: search below {} with {} found {} entries. {}", |
| config.getGroupConfig().getBaseDN(), searchFilter, groups.size(), timer.getString()); |
| } |
| return groups; |
| } catch (Exception e) { |
| log.error("Error during ldap membership search." ,e); |
| throw new ExternalIdentityException("Error during ldap membership search.", e); |
| } finally { |
| if (searchCursor != null) { |
| try { |
| searchCursor.close(); |
| } catch (IOException e) { |
| log.warn("Failed to close search cursor.", e); |
| } |
| } |
| disconnect(connection); |
| } |
| } |
| |
| /** |
| * Collects the declared (direct) members of a group |
| * @param ref the reference to the group |
| * @return map of identity refers |
| * @throws ExternalIdentityException if an error occurs |
| */ |
| Map<String, ExternalIdentityRef> getDeclaredMemberRefs(ExternalIdentityRef ref, String dn) throws ExternalIdentityException { |
| if (!isMyRef(ref)) { |
| return Collections.emptyMap(); |
| } |
| LdapConnection connection = null; |
| try { |
| Map<String, ExternalIdentityRef> members = new HashMap<String, ExternalIdentityRef>(); |
| DebugTimer timer = new DebugTimer(); |
| connection = connect(); |
| timer.mark("connect"); |
| Entry entry = connection.lookup(dn); |
| timer.mark("lookup"); |
| Attribute attr = entry.get(config.getGroupMemberAttribute()); |
| if (attr == null) { |
| log.warn("LDAP group does not have configured attribute: {}", config.getGroupMemberAttribute()); |
| } else { |
| for (Value value: attr) { |
| ExternalIdentityRef memberRef = new ExternalIdentityRef(value.getString(), this.getName()); |
| members.put(memberRef.getId(), memberRef); |
| } |
| } |
| timer.mark("iterate"); |
| if (log.isDebugEnabled()) { |
| log.debug("members lookup of {} found {} members. {}", ref.getId(), members.size(), timer.getString()); |
| } |
| return members; |
| } catch (Exception e) { |
| String msg = "Error during ldap group members lookup."; |
| log.error(msg ,e); |
| throw new ExternalIdentityException(msg, e); |
| } finally { |
| disconnect(connection); |
| } |
| } |
| |
| //------------------------------------------------------------< private >--- |
| /** |
| * Initializes the ldap identity provider. |
| */ |
| private void init() { |
| if (adminConnectionFactory != null) { |
| throw new IllegalStateException("Provider already initialized."); |
| } |
| |
| // make sure the JVM supports the TLSv1.1 |
| try { |
| enabledSSLProtocols = null; |
| SSLContext.getInstance("TLSv1.1"); |
| } catch (NoSuchAlgorithmException e) { |
| log.warn("JDK does not support TLSv1.1. Disabling it."); |
| enabledSSLProtocols = new String[]{"TLSv1"}; |
| } |
| |
| // setup admin connection pool |
| LdapConnectionConfig cc = createConnectionConfig(); |
| String bindDN = config.getBindDN(); |
| if (bindDN != null && !bindDN.isEmpty()) { |
| cc.setName(bindDN); |
| cc.setCredentials(config.getBindPassword()); |
| } |
| adminConnectionFactory = new ValidatingPoolableLdapConnectionFactory(cc); |
| if (config.getAdminPoolConfig().lookupOnValidate()) { |
| adminConnectionFactory.setValidator(new LookupLdapConnectionValidator()); |
| } else { |
| adminConnectionFactory.setValidator(new DefaultLdapConnectionValidator()); |
| } |
| if (config.getAdminPoolConfig().getMaxActive() != 0) { |
| adminPool = new LdapConnectionPool(adminConnectionFactory); |
| adminPool.setTestOnBorrow(true); |
| adminPool.setMaxActive(config.getAdminPoolConfig().getMaxActive()); |
| adminPool.setWhenExhaustedAction(GenericObjectPool.WHEN_EXHAUSTED_BLOCK); |
| } |
| |
| // setup unbound connection pool. let's create a new version of the config |
| cc = createConnectionConfig(); |
| |
| userConnectionFactory = new PoolableUnboundConnectionFactory(cc); |
| if (config.getUserPoolConfig().lookupOnValidate()) { |
| userConnectionFactory.setValidator(new UnboundLookupConnectionValidator()); |
| } else { |
| userConnectionFactory.setValidator(new UnboundConnectionValidator()); |
| } |
| if (config.getUserPoolConfig().getMaxActive() != 0) { |
| userPool = new UnboundLdapConnectionPool(userConnectionFactory); |
| userPool.setTestOnBorrow(true); |
| userPool.setMaxActive(config.getUserPoolConfig().getMaxActive()); |
| userPool.setWhenExhaustedAction(GenericObjectPool.WHEN_EXHAUSTED_BLOCK); |
| } |
| |
| log.info("LdapIdentityProvider initialized: {}", config); |
| } |
| |
| /** |
| * Creates a new connection config based on the config. |
| * @return the connection config. |
| */ |
| @NotNull |
| private LdapConnectionConfig createConnectionConfig() { |
| LdapConnectionConfig cc = new LdapConnectionConfig(); |
| cc.setLdapHost(config.getHostname()); |
| cc.setLdapPort(config.getPort()); |
| cc.setUseSsl(config.useSSL()); |
| cc.setUseTls(config.useTLS()); |
| |
| // todo: implement better trustmanager/keystore management (via sling/felix) |
| if (config.noCertCheck()) { |
| cc.setTrustManagers(new NoVerificationTrustManager()); |
| } |
| |
| if (enabledSSLProtocols != null) { |
| cc.setEnabledProtocols(enabledSSLProtocols); |
| } |
| |
| return cc; |
| } |
| |
| @Nullable |
| private Entry getEntry(@NotNull LdapConnection connection, @NotNull LdapProviderConfig.Identity idConfig, @NotNull String id, @NotNull String[] customAttributes) |
| throws CursorException, LdapException { |
| String searchFilter = idConfig.getSearchFilter(id); |
| |
| // Create the SearchRequest object |
| SearchRequest req = new SearchRequestImpl(); |
| req.setScope(SearchScope.SUBTREE); |
| if (customAttributes.length == 0) { |
| req.addAttributes(SchemaConstants.ALL_USER_ATTRIBUTES); |
| } else { |
| req.addAttributes(customAttributes); |
| } |
| req.setTimeLimit((int) config.getSearchTimeout()); |
| req.setBase(new Dn(idConfig.getBaseDN())); |
| req.setFilter(searchFilter); |
| |
| if (log.isDebugEnabled()) { |
| log.debug("getEntry: using SearchRequest {}.", req); |
| } |
| |
| // Process the request |
| SearchCursor searchCursor = null; |
| Entry resultEntry = null; |
| try { |
| searchCursor = connection.search(req); |
| while (searchCursor.next()) { |
| if (resultEntry != null) { |
| log.warn("search for {} returned more than one entry. discarding additional ones.", searchFilter); |
| } else { |
| // process the SearchResultEntry |
| Response response = searchCursor.get(); |
| if (response instanceof SearchResultEntry) { |
| resultEntry = ((SearchResultEntry) response).getEntry(); |
| } |
| } |
| } |
| } finally { |
| if (searchCursor != null) { |
| try { |
| searchCursor.close(); |
| } catch (IOException e) { |
| log.warn("Failed to close search cursor.", e); |
| } |
| } |
| } |
| if (log.isDebugEnabled()) { |
| if (resultEntry == null) { |
| log.debug("getEntry: search below {} with {} found 0 entries.", idConfig.getBaseDN(), searchFilter); |
| } else { |
| log.debug("getEntry: search below {} with {} found {}", idConfig.getBaseDN(), searchFilter, resultEntry.getDn()); |
| } |
| } |
| return resultEntry; |
| } |
| |
| |
| @NotNull |
| private SearchResultIterator getEntryIterator(@NotNull LdapProviderConfig.Identity idConfig) throws LdapException, CursorException, ExternalIdentityException { |
| StringBuilder filter = new StringBuilder(); |
| int num = 0; |
| for (String objectClass: idConfig.getObjectClasses()) { |
| num++; |
| filter.append("(objectclass=") |
| .append(LdapProviderConfig.encodeFilterValue(objectClass)) |
| .append(')'); |
| } |
| String extraFilter = idConfig.getExtraFilter(); |
| if (extraFilter != null && !extraFilter.isEmpty()) { |
| num++; |
| filter.append(extraFilter); |
| } |
| String searchFilter = num > 1 |
| ? "(&" + filter + ')' |
| : filter.toString(); |
| |
| return new SearchResultIterator(searchFilter, idConfig); |
| } |
| |
| private final class SearchResultIterator implements Iterator<Entry> { |
| |
| private final String searchFilter; |
| private final LdapProviderConfig.Identity idConfig; |
| |
| private byte[] cookie; |
| private List page = Collections.emptyList(); |
| private boolean searchComplete; |
| private int pos = -1; |
| |
| public SearchResultIterator( |
| @NotNull String searchFilter, |
| @NotNull LdapProviderConfig.Identity idConfig) throws LdapException, CursorException, ExternalIdentityException { |
| this.searchFilter = searchFilter; |
| this.idConfig = idConfig; |
| findNextEntry(); |
| } |
| |
| //-------------------------------------------------------< Iterator >--- |
| |
| @Override |
| public boolean hasNext() { |
| return pos >= 0; |
| } |
| |
| @Override |
| public Entry next() { |
| if (hasNext()) { |
| try { |
| Entry entry = (Entry) page.get(pos); |
| findNextEntry(); |
| return entry; |
| } catch (LdapException | CursorException | ExternalIdentityException e) { |
| log.error("Error while performing LDAP search", e); |
| } |
| } |
| throw new NoSuchElementException(); |
| } |
| |
| @Override |
| public void remove() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| //-------------------------------------------------------< internal >--- |
| |
| private SearchRequest createSearchRequest(LdapConnection connection, byte[] cookie, @NotNull String[] userAttributes) throws LdapException { |
| SearchRequest req = new SearchRequestImpl(); |
| req.setScope(SearchScope.SUBTREE); |
| if (userAttributes.length == 0) { |
| req.addAttributes(SchemaConstants.ALL_USER_ATTRIBUTES); |
| } else { |
| req.addAttributes(userAttributes); |
| } |
| req.setTimeLimit((int) config.getSearchTimeout()); |
| req.setBase(new Dn(idConfig.getBaseDN())); |
| req.setFilter(searchFilter); |
| |
| PagedResults pagedSearchControl = new PagedResultsDecorator(connection.getCodecService()); |
| // do paged searches (OAK-2874) |
| pagedSearchControl.setSize(1000); |
| pagedSearchControl.setCookie(cookie); |
| req.addControl(pagedSearchControl); |
| |
| return req; |
| } |
| |
| private boolean loadNextPage() throws ExternalIdentityException, LdapException, CursorException { |
| if (searchComplete) { |
| return false; |
| } |
| SearchCursor searchCursor = null; |
| DebugTimer timer = new DebugTimer(); |
| LdapConnection connection = connect(); |
| timer.mark("connect"); |
| page = new ArrayList<Entry>(); |
| try { |
| SearchRequest req = createSearchRequest(connection, cookie, config.getCustomAttributes()); |
| if (log.isDebugEnabled()) { |
| log.debug("loadNextPage: using SearchRequest {}.", req); |
| } |
| searchCursor = connection.search(req); |
| while (searchCursor.next()) { |
| Response response = searchCursor.get(); |
| |
| if (response instanceof SearchResultEntry) { |
| Entry resultEntry = ((SearchResultEntry) response).getEntry(); |
| page.add(resultEntry); |
| if (log.isDebugEnabled()) { |
| log.debug("loadNextPage: search below {} with {} found {}", idConfig.getBaseDN(), searchFilter, resultEntry.getDn()); |
| } |
| } |
| } |
| |
| SearchResultDone done = searchCursor.getSearchResultDone(); |
| cookie = null; |
| if (done.getLdapResult().getResultCode() != ResultCodeEnum.UNWILLING_TO_PERFORM) { |
| |
| PagedResults ctrl = (PagedResults) done.getControl(PagedResults.OID); |
| if (ctrl != null) { |
| cookie = ctrl.getCookie(); |
| } |
| } |
| searchComplete = cookie == null; |
| timer.mark("lookup"); |
| |
| return !page.isEmpty(); |
| } finally { |
| if (searchCursor != null) { |
| try { |
| searchCursor.close(); |
| } catch (IOException e) { |
| log.warn("Failed to close search cursor.", e); |
| } |
| } |
| disconnect(connection); |
| } |
| } |
| |
| private void findNextEntry() throws LdapException, CursorException, ExternalIdentityException { |
| if (pos == -1 && !loadNextPage()) { |
| return; |
| } |
| if (pos + 1 == page.size()) { |
| pos = -1; |
| page = Collections.emptyList(); |
| if (!loadNextPage()) { |
| return; |
| } |
| } |
| pos++; |
| } |
| } |
| |
| @NotNull |
| private ExternalUser createUser(@NotNull Entry entry, @Nullable String id) |
| throws LdapInvalidAttributeValueException { |
| return (ExternalUser) createIdentity(entry, id, false); |
| } |
| |
| @NotNull |
| private ExternalGroup createGroup(@NotNull Entry entry, @Nullable String id) |
| throws LdapInvalidAttributeValueException { |
| return (ExternalGroup) createIdentity(entry, id, true); |
| } |
| |
| @NotNull |
| private ExternalIdentity createIdentity(@NotNull Entry entry, @Nullable String id, boolean isGroup) |
| throws LdapInvalidAttributeValueException { |
| LdapProviderConfig.Identity cfg = isGroup ? config.getGroupConfig() : config.getUserConfig(); |
| if (id == null) { |
| String idAttribute = cfg.getIdAttribute(); |
| Attribute attr = entry.get(idAttribute); |
| if (attr == null) { |
| throw new LdapInvalidAttributeValueException(ResultCodeEnum.CONSTRAINT_VIOLATION, |
| "no value found for attribute '" + idAttribute + "' for entry " + entry); |
| } |
| id = attr.getString(); |
| } |
| String extId = config.getUseUidForExtId() ? id : entry.getDn().getName(); |
| ExternalIdentityRef ref = new ExternalIdentityRef(extId, this.getName()); |
| String path = cfg.makeDnPath() |
| ? createDNPath(entry.getDn()) |
| : null; |
| LdapIdentity identity = isGroup ? new LdapGroup(this, ref, id, path, entry) |
| : new LdapUser(this, ref, id, path, entry); |
| Map<String, Object> props = identity.getProperties(); |
| applyAttributes(props, entry); |
| return identity; |
| } |
| |
| private void applyAttributes(Map<String, Object> props, Entry entry) |
| throws LdapInvalidAttributeValueException { |
| for (Attribute attr: entry.getAttributes()) { |
| if (attr.isHumanReadable()) { |
| final Object propValue; |
| // for multivalue properties, store as collection |
| if (attr.size() > 1) { |
| List<String> values = new ArrayList(); |
| for (Value<?> value : attr) { |
| values.add(value.getString()); |
| } |
| propValue = values; |
| } else { |
| propValue = attr.getString(); |
| } |
| props.put(attr.getId(), propValue); |
| } |
| } |
| } |
| |
| @NotNull |
| private LdapConnection connect() throws ExternalIdentityException { |
| try { |
| if (adminPool == null) { |
| return adminConnectionFactory.makeObject(); |
| } else { |
| return adminPool.getConnection(); |
| } |
| } catch (Exception e) { |
| String msg = "Error while connecting to the ldap server."; |
| log.error(msg, e); |
| throw new ExternalIdentityException(msg, e); |
| } |
| } |
| |
| private void disconnect(@Nullable LdapConnection connection) { |
| try { |
| if (connection != null) { |
| if (adminPool == null) { |
| adminConnectionFactory.destroyObject(connection); |
| } else { |
| adminPool.releaseConnection(connection); |
| } |
| } |
| } catch (Exception e) { |
| log.warn("Error while disconnecting from the ldap server.", e); |
| } |
| } |
| |
| private boolean isMyRef(@NotNull ExternalIdentityRef ref) { |
| final String refProviderName = ref.getProviderName(); |
| return refProviderName == null || refProviderName.isEmpty() || getName().equals(refProviderName); |
| } |
| |
| /** |
| * Makes the intermediate path of an DN by splitting along the RDNs |
| * @param dn the dn of the identity |
| * @return the intermediate path or {@code null} if disabled by config |
| */ |
| private static String createDNPath(Dn dn) { |
| StringBuilder path = new StringBuilder(); |
| for (Rdn rnd: dn.getRdns()) { |
| if (path.length() > 0) { |
| path.append('/'); |
| } |
| path.append(Text.escapeIllegalJcrChars(rnd.toString())); |
| } |
| return path.toString(); |
| } |
| |
| private static ExternalIdentityException lookupFailedException(@NotNull Exception e, @Nullable DebugTimer timer) { |
| String msg = "Error during ldap lookup. "; |
| log.error(msg + ((timer != null) ? timer.getString() : ""), e); |
| return new ExternalIdentityException(msg, e); |
| } |
| } |