001/*
002 * (C) Copyright 2009 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Anahide Tchertchian
016 */
017
018package org.nuxeo.ecm.directory.ldap;
019
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.List;
023import java.util.Set;
024import java.util.TreeSet;
025
026import javax.naming.InvalidNameException;
027import javax.naming.NamingEnumeration;
028import javax.naming.NamingException;
029import javax.naming.directory.Attribute;
030import javax.naming.directory.Attributes;
031import javax.naming.directory.SearchControls;
032import javax.naming.directory.SearchResult;
033import javax.naming.ldap.LdapName;
034import javax.naming.ldap.Rdn;
035
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.common.utils.StringUtils;
039import org.nuxeo.common.xmap.annotation.XNode;
040import org.nuxeo.common.xmap.annotation.XObject;
041import org.nuxeo.ecm.directory.AbstractReference;
042import org.nuxeo.ecm.directory.Directory;
043import org.nuxeo.ecm.directory.DirectoryException;
044
045/**
046 * Implementation of the directory Reference interface that makes it possible to retrieve children of a node in the LDAP
047 * tree structure.
048 *
049 * @author Anahide Tchertchian
050 */
051@XObject(value = "ldapTreeReference")
052public class LDAPTreeReference extends AbstractReference {
053
054    private static final Log log = LogFactory.getLog(LDAPTreeReference.class);
055
056    public static final List<String> EMPTY_STRING_LIST = Collections.emptyList();
057
058    protected LDAPDirectoryDescriptor targetDirectoryDescriptor;
059
060    protected int scope;
061
062    @XNode("@field")
063    public void setFieldName(String fieldName) {
064        this.fieldName = fieldName;
065    }
066
067    protected LDAPFilterMatcher getFilterMatcher() {
068        return new LDAPFilterMatcher();
069    }
070
071    @Override
072    @XNode("@directory")
073    public void setTargetDirectoryName(String targetDirectoryName) {
074        this.targetDirectoryName = targetDirectoryName;
075    }
076
077    public int getScope() {
078        return scope;
079    }
080
081    @XNode("@scope")
082    public void setScope(String scope) throws DirectoryException {
083        if (scope == null) {
084            // default value: onelevel
085            this.scope = SearchControls.ONELEVEL_SCOPE;
086            return;
087        }
088        Integer searchScope = LdapScope.getIntegerScope(scope);
089        if (searchScope == null) {
090            // invalid scope
091            throw new DirectoryException("Invalid search scope: " + scope
092                    + ". Valid options: object, onelevel, subtree");
093        }
094        this.scope = searchScope.intValue();
095    }
096
097    @Override
098    public Directory getSourceDirectory() throws DirectoryException {
099
100        Directory sourceDir = super.getSourceDirectory();
101        if (sourceDir instanceof LDAPDirectory) {
102            return sourceDir;
103        } else {
104            throw new DirectoryException(sourceDirectoryName
105                    + " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName);
106        }
107    }
108
109    @Override
110    public Directory getTargetDirectory() throws DirectoryException {
111        Directory targetDir = super.getTargetDirectory();
112        if (targetDir instanceof LDAPDirectory) {
113            return targetDir;
114        } else {
115            throw new DirectoryException(targetDirectoryName
116                    + " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName);
117        }
118    }
119
120    protected LDAPDirectory getTargetLDAPDirectory() throws DirectoryException {
121        return (LDAPDirectory) getTargetDirectory();
122    }
123
124    protected LDAPDirectory getSourceLDAPDirectory() throws DirectoryException {
125        return (LDAPDirectory) getSourceDirectory();
126    }
127
128    protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() throws DirectoryException {
129        if (targetDirectoryDescriptor == null) {
130            targetDirectoryDescriptor = getTargetLDAPDirectory().getConfig();
131        }
132        return targetDirectoryDescriptor;
133    }
134
135    /**
136     * NOT IMPLEMENTED: Store new links
137     *
138     * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List)
139     */
140    @Override
141    public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException {
142        // TODO: not yet implemented
143    }
144
145    /**
146     * NOT IMPLEMENTED: Store new links.
147     *
148     * @see org.nuxeo.ecm.directory.Reference#addLinks(List, String)
149     */
150    @Override
151    public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException {
152        // TODO: not yet implemented
153    }
154
155    /**
156     * Fetches single parent, cutting the dn and trying to get the given entry.
157     *
158     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
159     */
160    @Override
161    public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException {
162        Set<String> sourceIds = new TreeSet<String>();
163        String targetDn = null;
164
165        // step #1: fetch the dn of the targetId entry in the target
166        // directory by the static dn valued strategy
167        LDAPDirectory targetDir = getTargetLDAPDirectory();
168        try (LDAPSession targetSession = (LDAPSession) targetDir.getSession()) {
169            SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId, true);
170            if (targetLdapEntry == null) {
171                // no parent accessible => return empty list
172                return EMPTY_STRING_LIST;
173            }
174            targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
175        } catch (NamingException e) {
176            throw new DirectoryException("error fetching " + targetId, e);
177        }
178
179        // step #2: search for entries that reference parent dn in the
180        // source directory and collect its id
181        LDAPDirectory sourceDirectory = getSourceLDAPDirectory();
182        String parentDn = getParentDn(targetDn);
183        String filterExpr = String.format("(&%s)", sourceDirectory.getBaseFilter());
184        String[] filterArgs = {};
185
186        // get a copy of original search controls
187        SearchControls sctls = sourceDirectory.getSearchControls(true);
188        sctls.setSearchScope(SearchControls.OBJECT_SCOPE);
189        try (LDAPSession sourceSession = (LDAPSession) sourceDirectory.getSession()) {
190            if (log.isDebugEnabled()) {
191                log.debug(String.format("LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
192                        + " filter='%s' args='%s' scope='%s' [%s]", targetId, parentDn, filterExpr,
193                        StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), this));
194            }
195            NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(parentDn, filterExpr, filterArgs,
196                    sctls);
197
198            try {
199                while (results.hasMore()) {
200                    Attributes attributes = results.next().getAttributes();
201                    // NXP-2461: check that id field is filled
202                    Attribute attr = attributes.get(sourceSession.idAttribute);
203                    if (attr != null) {
204                        Object value = attr.get();
205                        if (value != null) {
206                            sourceIds.add(value.toString());
207                            // only supposed to get one result anyway
208                            break;
209                        }
210                    }
211                }
212            } finally {
213                results.close();
214            }
215        } catch (NamingException e) {
216            throw new DirectoryException("error during reference search for " + targetDn, e);
217        }
218
219        return new ArrayList<String>(sourceIds);
220    }
221
222    /**
223     * Fetches children, onelevel or subtree given the reference configuration.
224     * <p>
225     * Removes entries with same id than parent to only get real children.
226     *
227     * @see org.nuxeo.ecm.directory.Reference#getTargetIdsForSource(String)
228     */
229    // TODO: optimize reusing the same ldap session (see LdapReference optim
230    // method)
231    @Override
232    public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException {
233        Set<String> targetIds = new TreeSet<String>();
234        String sourceDn = null;
235
236        // step #1: fetch the dn of the sourceId entry in the source
237        // directory by the static dn valued strategy
238        LDAPDirectory sourceDir = getSourceLDAPDirectory();
239        try (LDAPSession sourceSession = (LDAPSession) sourceDir.getSession()) {
240            SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId, true);
241            if (sourceLdapEntry == null) {
242                throw new DirectoryException(sourceId + " does not exist in " + sourceDirectoryName);
243            }
244            sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace());
245        } catch (NamingException e) {
246            throw new DirectoryException("error fetching " + sourceId, e);
247        }
248
249        // step #2: search for entries with sourceDn as base dn and collect
250        // their ids
251        LDAPDirectory targetDirectory = getTargetLDAPDirectory();
252
253        String filterExpr = String.format("(&%s)", targetDirectory.getBaseFilter());
254        String[] filterArgs = {};
255
256        // get a copy of original search controls
257        SearchControls sctls = targetDirectory.getSearchControls(true);
258        sctls.setSearchScope(getScope());
259        try (LDAPSession targetSession = (LDAPSession) targetDirectory.getSession()) {
260            if (log.isDebugEnabled()) {
261                log.debug(String.format("LDAPReference.getTargetIdsForSource(%s): LDAP search search base='%s'"
262                        + " filter='%s' args='%s' scope='%s' [%s]", sourceId, sourceDn, filterExpr,
263                        StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), this));
264            }
265            NamingEnumeration<SearchResult> results = targetSession.dirContext.search(sourceDn, filterExpr, filterArgs,
266                    sctls);
267
268            try {
269                while (results.hasMore()) {
270                    Attributes attributes = results.next().getAttributes();
271                    // NXP-2461: check that id field is filled
272                    Attribute attr = attributes.get(targetSession.idAttribute);
273                    if (attr != null) {
274                        Object value = attr.get();
275                        if (value != null) {
276                            // always remove self as child
277                            String targetId = value.toString();
278                            if (!sourceId.equals(targetId)) {
279                                targetIds.add(targetId);
280                            }
281                        }
282                    }
283                }
284            } finally {
285                results.close();
286            }
287        } catch (NamingException e) {
288            throw new DirectoryException("error during reference search for " + sourceDn, e);
289        }
290
291        return new ArrayList<String>(targetIds);
292    }
293
294    /**
295     * Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for
296     * comparison purpose.
297     *
298     * @param dn the raw unnormalized dn
299     * @return lowercase version without whitespace after commas
300     */
301    protected static String pseudoNormalizeDn(String dn) throws InvalidNameException {
302        LdapName ldapName = new LdapName(dn);
303        List<String> rdns = new ArrayList<String>();
304        for (Rdn rdn : ldapName.getRdns()) {
305            String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,");
306            String rdnStr = rdn.getType().toLowerCase() + "=" + value;
307            rdns.add(0, rdnStr);
308        }
309        return StringUtils.join(rdns, ',');
310    }
311
312    protected String getParentDn(String dn) {
313        LdapName ldapName;
314        String parentDn;
315
316        if (dn != null) {
317            try {
318                ldapName = new LdapName(dn);
319                ldapName.remove(ldapName.size() - 1);
320                parentDn = ldapName.toString();
321                return parentDn;
322
323            } catch (InvalidNameException ex) {
324                return null;
325            }
326        }
327        return null;
328    }
329
330    /**
331     * NOT IMPLEMENTED: Remove existing statically defined links for the given source id
332     *
333     * @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String)
334     */
335    @Override
336    public void removeLinksForSource(String sourceId) throws DirectoryException {
337        // TODO: not yet implemented
338    }
339
340    /**
341     * NOT IMPLEMENTED: Remove existing statically defined links for the given target id
342     *
343     * @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String)
344     */
345    @Override
346    public void removeLinksForTarget(String targetId) throws DirectoryException {
347        // TODO: not yet implemented
348    }
349
350    /**
351     * NOT IMPLEMENTED: Edit the list of statically defined references for a given target
352     *
353     * @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List)
354     */
355    @Override
356    public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException {
357        // TODO: not yet implemented
358    }
359
360    /**
361     * NOT IMPLEMENTED: Set the list of statically defined references for a given source
362     *
363     * @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List)
364     */
365    @Override
366    public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException {
367        // TODO: not yet implemented
368    }
369
370    @Override
371    // to build helpful debug logs
372    public String toString() {
373        return String.format("LDAPTreeReference to resolve field='%s' of sourceDirectory='%s'"
374                + " with targetDirectory='%s'", fieldName, sourceDirectoryName, targetDirectoryName);
375    }
376
377    @Override
378    protected AbstractReference newInstance() {
379        return new LDAPTreeReference();
380    }
381
382    /**
383     * @since 5.6
384     */
385    @Override
386    public LDAPTreeReference clone() {
387        LDAPTreeReference clone = (LDAPTreeReference) super.clone();
388        clone.scope = scope;
389        return clone;
390    }
391
392}