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