/* * (C) Copyright 2009-2016 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * Contributors: * Anahide Tchertchian */ package org.nuxeo.ecm.directory.ldap; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.naming.InvalidNameException; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.xmap.annotation.XNode; import org.nuxeo.common.xmap.annotation.XObject; import org.nuxeo.ecm.directory.AbstractReference; import org.nuxeo.ecm.directory.Directory; import org.nuxeo.ecm.directory.DirectoryException; /** * Implementation of the directory Reference interface that makes it possible to retrieve children of a node in the LDAP * tree structure. * * @author Anahide Tchertchian */ @XObject(value = "ldapTreeReference") public class LDAPTreeReference extends AbstractReference { private static final Log log = LogFactory.getLog(LDAPTreeReference.class); public static final List<String> EMPTY_STRING_LIST = Collections.emptyList(); protected LDAPDirectoryDescriptor targetDirectoryDescriptor; protected int scope; @XNode("@field") public void setFieldName(String fieldName) { this.fieldName = fieldName; } protected LDAPFilterMatcher getFilterMatcher() { return new LDAPFilterMatcher(); } @Override @XNode("@directory") public void setTargetDirectoryName(String targetDirectoryName) { this.targetDirectoryName = targetDirectoryName; } public int getScope() { return scope; } @XNode("@scope") public void setScope(String scope) throws DirectoryException { if (scope == null) { // default value: onelevel this.scope = SearchControls.ONELEVEL_SCOPE; return; } Integer searchScope = LdapScope.getIntegerScope(scope); if (searchScope == null) { // invalid scope throw new DirectoryException("Invalid search scope: " + scope + ". Valid options: object, onelevel, subtree"); } this.scope = searchScope.intValue(); } @Override public Directory getSourceDirectory() throws DirectoryException { Directory sourceDir = super.getSourceDirectory(); if (sourceDir instanceof LDAPDirectory) { return sourceDir; } else { throw new DirectoryException(sourceDirectoryName + " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName); } } @Override public Directory getTargetDirectory() throws DirectoryException { Directory targetDir = super.getTargetDirectory(); if (targetDir instanceof LDAPDirectory) { return targetDir; } else { throw new DirectoryException(targetDirectoryName + " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName); } } protected LDAPDirectory getTargetLDAPDirectory() throws DirectoryException { return (LDAPDirectory) getTargetDirectory(); } protected LDAPDirectory getSourceLDAPDirectory() throws DirectoryException { return (LDAPDirectory) getSourceDirectory(); } protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() throws DirectoryException { if (targetDirectoryDescriptor == null) { targetDirectoryDescriptor = getTargetLDAPDirectory().getDescriptor(); } return targetDirectoryDescriptor; } /** * NOT IMPLEMENTED: Store new links * * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List) */ @Override public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException { // TODO: not yet implemented } /** * NOT IMPLEMENTED: Store new links. * * @see org.nuxeo.ecm.directory.Reference#addLinks(List, String) */ @Override public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException { // TODO: not yet implemented } /** * Fetches single parent, cutting the dn and trying to get the given entry. * * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String) */ @Override public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException { Set<String> sourceIds = new TreeSet<>(); String targetDn = null; // step #1: fetch the dn of the targetId entry in the target // directory by the static dn valued strategy LDAPDirectory targetDir = getTargetLDAPDirectory(); try (LDAPSession targetSession = (LDAPSession) targetDir.getSession()) { SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId, true); if (targetLdapEntry == null) { // no parent accessible => return empty list return EMPTY_STRING_LIST; } targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace()); } catch (NamingException e) { throw new DirectoryException("error fetching " + targetId, e); } // step #2: search for entries that reference parent dn in the // source directory and collect its id LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory(); String parentDn = getParentDn(targetDn); String filterExpr = String.format("(&%s)", ldapSourceDirectory.getBaseFilter()); String[] filterArgs = {}; // get a copy of original search controls SearchControls sctls = ldapSourceDirectory.getSearchControls(true); sctls.setSearchScope(SearchControls.OBJECT_SCOPE); try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) { if (log.isDebugEnabled()) { log.debug(String.format("LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'" + " filter='%s' args='%s' scope='%s' [%s]", targetId, parentDn, filterExpr, StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), this)); } NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(parentDn, filterExpr, filterArgs, sctls); try { while (results.hasMore()) { Attributes attributes = results.next().getAttributes(); // NXP-2461: check that id field is filled Attribute attr = attributes.get(sourceSession.idAttribute); if (attr != null) { Object value = attr.get(); if (value != null) { sourceIds.add(value.toString()); // only supposed to get one result anyway break; } } } } finally { results.close(); } } catch (NamingException e) { throw new DirectoryException("error during reference search for " + targetDn, e); } return new ArrayList<>(sourceIds); } /** * Fetches children, onelevel or subtree given the reference configuration. * <p> * Removes entries with same id than parent to only get real children. * * @see org.nuxeo.ecm.directory.Reference#getTargetIdsForSource(String) */ // TODO: optimize reusing the same ldap session (see LdapReference optim // method) @Override public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException { Set<String> targetIds = new TreeSet<>(); String sourceDn = null; // step #1: fetch the dn of the sourceId entry in the source // directory by the static dn valued strategy LDAPDirectory sourceDir = getSourceLDAPDirectory(); try (LDAPSession sourceSession = (LDAPSession) sourceDir.getSession()) { SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId, true); if (sourceLdapEntry == null) { throw new DirectoryException(sourceId + " does not exist in " + sourceDirectoryName); } sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace()); } catch (NamingException e) { throw new DirectoryException("error fetching " + sourceId, e); } // step #2: search for entries with sourceDn as base dn and collect // their ids LDAPDirectory ldapTargetDirectory = getTargetLDAPDirectory(); String filterExpr = String.format("(&%s)", ldapTargetDirectory.getBaseFilter()); String[] filterArgs = {}; // get a copy of original search controls SearchControls sctls = ldapTargetDirectory.getSearchControls(true); sctls.setSearchScope(getScope()); try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) { if (log.isDebugEnabled()) { log.debug(String.format("LDAPReference.getTargetIdsForSource(%s): LDAP search search base='%s'" + " filter='%s' args='%s' scope='%s' [%s]", sourceId, sourceDn, filterExpr, StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), this)); } NamingEnumeration<SearchResult> results = targetSession.dirContext.search(sourceDn, filterExpr, filterArgs, sctls); try { while (results.hasMore()) { Attributes attributes = results.next().getAttributes(); // NXP-2461: check that id field is filled Attribute attr = attributes.get(targetSession.idAttribute); if (attr != null) { Object value = attr.get(); if (value != null) { // always remove self as child String targetId = value.toString(); if (!sourceId.equals(targetId)) { targetIds.add(targetId); } } } } } finally { results.close(); } } catch (NamingException e) { throw new DirectoryException("error during reference search for " + sourceDn, e); } return new ArrayList<>(targetIds); } /** * Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for * comparison purpose. * * @param dn the raw unnormalized dn * @return lowercase version without whitespace after commas */ protected static String pseudoNormalizeDn(String dn) throws InvalidNameException { LdapName ldapName = new LdapName(dn); List<String> rdns = new ArrayList<>(); for (Rdn rdn : ldapName.getRdns()) { String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,"); String rdnStr = rdn.getType().toLowerCase() + "=" + value; rdns.add(0, rdnStr); } return StringUtils.join(rdns, ','); } protected String getParentDn(String dn) { LdapName ldapName; String parentDn; if (dn != null) { try { ldapName = new LdapName(dn); ldapName.remove(ldapName.size() - 1); parentDn = ldapName.toString(); return parentDn; } catch (InvalidNameException ex) { return null; } } return null; } /** * NOT IMPLEMENTED: Remove existing statically defined links for the given source id * * @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String) */ @Override public void removeLinksForSource(String sourceId) throws DirectoryException { // TODO: not yet implemented } /** * NOT IMPLEMENTED: Remove existing statically defined links for the given target id * * @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String) */ @Override public void removeLinksForTarget(String targetId) throws DirectoryException { // TODO: not yet implemented } /** * NOT IMPLEMENTED: Edit the list of statically defined references for a given target * * @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List) */ @Override public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException { // TODO: not yet implemented } /** * NOT IMPLEMENTED: Set the list of statically defined references for a given source * * @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List) */ @Override public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException { // TODO: not yet implemented } @Override // to build helpful debug logs public String toString() { return String.format("LDAPTreeReference to resolve field='%s' of sourceDirectory='%s'" + " with targetDirectory='%s'", fieldName, sourceDirectoryName, targetDirectoryName); } /** * @since 5.6 */ @Override public LDAPTreeReference clone() { LDAPTreeReference clone = (LDAPTreeReference) super.clone(); // basic fields are already copied by super.clone() return clone; } }