001/*
002 * (C) Copyright 2009-2018 Nuxeo (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.lang3.StringUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
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;
046import org.nuxeo.ecm.directory.ReferenceDescriptor;
047import org.nuxeo.ecm.directory.Session;
048
049/**
050 * Implementation of the directory Reference interface that makes it possible to retrieve children of a node in the LDAP
051 * tree structure.
052 *
053 * @author Anahide Tchertchian
054 */
055@XObject(value = "ldapTreeReference")
056public class LDAPTreeReference extends AbstractReference implements Cloneable {
057
058    private static final Log log = LogFactory.getLog(LDAPTreeReference.class);
059
060    public static final List<String> EMPTY_STRING_LIST = Collections.emptyList();
061
062    protected LDAPDirectoryDescriptor targetDirectoryDescriptor;
063
064    protected int scope;
065
066    public LDAPTreeReference() {
067        super(null, null);
068    }
069
070    public LDAPTreeReference(ReferenceDescriptor referenceDescriptor) {
071        super(referenceDescriptor.getFieldName(), referenceDescriptor.getDirectory());
072    }
073
074    @XNode("@field")
075    public void setFieldName(String fieldName) {
076        this.fieldName = fieldName;
077    }
078
079    protected LDAPFilterMatcher getFilterMatcher() {
080        return new LDAPFilterMatcher();
081    }
082
083    @Override
084    @XNode("@directory")
085    public void setTargetDirectoryName(String targetDirectoryName) {
086        this.targetDirectoryName = targetDirectoryName;
087    }
088
089    public int getScope() {
090        return scope;
091    }
092
093    @XNode("@scope")
094    public void setScope(String scope) {
095        if (scope == null) {
096            // default value: onelevel
097            this.scope = SearchControls.ONELEVEL_SCOPE;
098            return;
099        }
100        Integer searchScope = LdapScope.getIntegerScope(scope);
101        if (searchScope == null) {
102            // invalid scope
103            throw new DirectoryException(
104                    "Invalid search scope: " + scope + ". Valid options: object, onelevel, subtree");
105        }
106        this.scope = searchScope.intValue();
107    }
108
109    @Override
110    public Directory getSourceDirectory() {
111
112        Directory sourceDir = super.getSourceDirectory();
113        if (sourceDir instanceof LDAPDirectory) {
114            return sourceDir;
115        } else {
116            throw new DirectoryException(sourceDirectoryName
117                    + " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName);
118        }
119    }
120
121    @Override
122    public Directory getTargetDirectory() {
123        Directory targetDir = super.getTargetDirectory();
124        if (targetDir instanceof LDAPDirectory) {
125            return targetDir;
126        } else {
127            throw new DirectoryException(targetDirectoryName
128                    + " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName);
129        }
130    }
131
132    protected LDAPDirectory getTargetLDAPDirectory() {
133        return (LDAPDirectory) getTargetDirectory();
134    }
135
136    protected LDAPDirectory getSourceLDAPDirectory() {
137        return (LDAPDirectory) getSourceDirectory();
138    }
139
140    protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() {
141        if (targetDirectoryDescriptor == null) {
142            targetDirectoryDescriptor = getTargetLDAPDirectory().getDescriptor();
143        }
144        return targetDirectoryDescriptor;
145    }
146
147    /**
148     * NOT IMPLEMENTED: Store new links
149     *
150     * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List)
151     */
152    @Override
153    public void addLinks(String sourceId, List<String> targetIds) {
154        // TODO: not yet implemented
155    }
156
157    /**
158     * NOT IMPLEMENTED: Store new links
159     *
160     * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List, Session)
161     */
162    @Override
163    public void addLinks(String sourceId, List<String> targetIds, Session session) {
164        // TODO: not yet implemented
165    }
166
167    @Override
168    public void addLinks(List<String> sourceIds, String targetId, Session session) {
169        // TODO: not yet implemented
170    }
171
172    /**
173     * NOT IMPLEMENTED: Store new links.
174     *
175     * @see org.nuxeo.ecm.directory.Reference#addLinks(List, String)
176     */
177    @Override
178    public void addLinks(List<String> sourceIds, String targetId) {
179        // TODO: not yet implemented
180    }
181
182    /**
183     * Fetches single parent, cutting the dn and trying to get the given entry.
184     *
185     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
186     */
187    @Override
188    public List<String> getSourceIdsForTarget(String targetId) {
189        Set<String> sourceIds = new TreeSet<>();
190        String targetDn;
191
192        // step #1: fetch the dn of the targetId entry in the target
193        // directory by the static dn valued strategy
194        LDAPDirectory targetDir = getTargetLDAPDirectory();
195        try (LDAPSession targetSession = targetDir.getSession()) {
196            SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId, true);
197            if (targetLdapEntry == null) {
198                // no parent accessible => return empty list
199                return EMPTY_STRING_LIST;
200            }
201            targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
202        } catch (NamingException e) {
203            throw new DirectoryException("error fetching " + targetId, e);
204        }
205
206        // step #2: search for entries that reference parent dn in the
207        // source directory and collect its id
208        LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory();
209        String parentDn = getParentDn(targetDn);
210        String filterExpr = String.format("(&%s)", ldapSourceDirectory.getBaseFilter());
211        String[] filterArgs = {};
212
213        // get a copy of original search controls
214        SearchControls sctls = ldapSourceDirectory.getSearchControls(true);
215        sctls.setSearchScope(SearchControls.OBJECT_SCOPE);
216        try (LDAPSession sourceSession = ldapSourceDirectory.getSession()) {
217            if (log.isDebugEnabled()) {
218                log.debug(String.format(
219                        "LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
220                                + " filter='%s' args='%s' scope='%s' [%s]",
221                        targetId, parentDn, filterExpr, StringUtils.join(filterArgs, ", "), sctls.getSearchScope(),
222                        this));
223            }
224            NamingEnumeration<SearchResult> results = sourceSession.getContext().search(parentDn, filterExpr,
225                    filterArgs, sctls);
226
227            try {
228                while (results.hasMore()) {
229                    Attributes attributes = results.next().getAttributes();
230                    // NXP-2461: check that id field is filled
231                    Attribute attr = attributes.get(sourceSession.idAttribute);
232                    if (attr != null) {
233                        Object value = attr.get();
234                        if (value != null) {
235                            sourceIds.add(value.toString());
236                            // only supposed to get one result anyway
237                            break;
238                        }
239                    }
240                }
241            } finally {
242                results.close();
243            }
244        } catch (NamingException e) {
245            throw new DirectoryException("error during reference search for " + targetDn, e);
246        }
247
248        return new ArrayList<>(sourceIds);
249    }
250
251    /**
252     * Fetches children, onelevel or subtree given the reference configuration.
253     * <p>
254     * Removes entries with same id than parent to only get real children.
255     *
256     * @see org.nuxeo.ecm.directory.Reference#getTargetIdsForSource(String)
257     */
258    // TODO: optimize reusing the same ldap session (see LdapReference optim
259    // method)
260    @Override
261    public List<String> getTargetIdsForSource(String sourceId) {
262        Set<String> targetIds = new TreeSet<>();
263        String sourceDn;
264
265        // step #1: fetch the dn of the sourceId entry in the source
266        // directory by the static dn valued strategy
267        LDAPDirectory sourceDir = getSourceLDAPDirectory();
268        try (LDAPSession sourceSession = sourceDir.getSession()) {
269            SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId, true);
270            if (sourceLdapEntry == null) {
271                throw new DirectoryException(sourceId + " does not exist in " + sourceDirectoryName);
272            }
273            sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace());
274        } catch (NamingException e) {
275            throw new DirectoryException("error fetching " + sourceId, e);
276        }
277
278        // step #2: search for entries with sourceDn as base dn and collect
279        // their ids
280        LDAPDirectory ldapTargetDirectory = getTargetLDAPDirectory();
281
282        String filterExpr = String.format("(&%s)", ldapTargetDirectory.getBaseFilter());
283        String[] filterArgs = {};
284
285        // get a copy of original search controls
286        SearchControls sctls = ldapTargetDirectory.getSearchControls(true);
287        sctls.setSearchScope(getScope());
288        try (LDAPSession targetSession = ldapTargetDirectory.getSession()) {
289            if (log.isDebugEnabled()) {
290                log.debug(String.format(
291                        "LDAPReference.getTargetIdsForSource(%s): LDAP search search base='%s'"
292                                + " filter='%s' args='%s' scope='%s' [%s]",
293                        sourceId, sourceDn, filterExpr, StringUtils.join(filterArgs, ", "), sctls.getSearchScope(),
294                        this));
295            }
296            NamingEnumeration<SearchResult> results = targetSession.getContext().search(sourceDn, filterExpr,
297                    filterArgs, sctls);
298
299            try {
300                while (results.hasMore()) {
301                    Attributes attributes = results.next().getAttributes();
302                    // NXP-2461: check that id field is filled
303                    Attribute attr = attributes.get(targetSession.idAttribute);
304                    if (attr != null) {
305                        Object value = attr.get();
306                        if (value != null) {
307                            // always remove self as child
308                            String targetId = value.toString();
309                            if (!sourceId.equals(targetId)) {
310                                targetIds.add(targetId);
311                            }
312                        }
313                    }
314                }
315            } finally {
316                results.close();
317            }
318        } catch (NamingException e) {
319            throw new DirectoryException("error during reference search for " + sourceDn, e);
320        }
321
322        return new ArrayList<>(targetIds);
323    }
324
325    /**
326     * Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for
327     * comparison purpose.
328     *
329     * @param dn the raw unnormalized dn
330     * @return lowercase version without whitespace after commas
331     */
332    protected static String pseudoNormalizeDn(String dn) throws InvalidNameException {
333        LdapName ldapName = new LdapName(dn);
334        List<String> rdns = new ArrayList<>();
335        for (Rdn rdn : ldapName.getRdns()) {
336            String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,");
337            String rdnStr = rdn.getType().toLowerCase() + "=" + value;
338            rdns.add(0, rdnStr);
339        }
340        return StringUtils.join(rdns, ',');
341    }
342
343    protected String getParentDn(String dn) {
344        LdapName ldapName;
345        String parentDn;
346
347        if (dn != null) {
348            try {
349                ldapName = new LdapName(dn);
350                ldapName.remove(ldapName.size() - 1);
351                parentDn = ldapName.toString();
352                return parentDn;
353
354            } catch (InvalidNameException ex) {
355                return null;
356            }
357        }
358        return null;
359    }
360
361    /**
362     * NOT IMPLEMENTED: Remove existing statically defined links for the given source id
363     *
364     * @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String)
365     */
366    @Override
367    public void removeLinksForSource(String sourceId) {
368        // TODO: not yet implemented
369    }
370
371    /**
372     * NOT IMPLEMENTED: Remove existing statically defined links for the given target id
373     *
374     * @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String)
375     */
376    @Override
377    public void removeLinksForTarget(String targetId) {
378        // TODO: not yet implemented
379    }
380
381    /**
382     * NOT IMPLEMENTED: Edit the list of statically defined references for a given target
383     *
384     * @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List)
385     */
386    @Override
387    public void setSourceIdsForTarget(String targetId, List<String> sourceIds) {
388        // TODO: not yet implemented
389    }
390
391    @Override
392    public void setSourceIdsForTarget(String targetId, List<String> sourceIds, Session session) {
393        // TODO: not yet implemented
394    }
395
396    /**
397     * NOT IMPLEMENTED: Set the list of statically defined references for a given source
398     *
399     * @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List)
400     */
401    @Override
402    public void setTargetIdsForSource(String sourceId, List<String> targetIds) {
403        // TODO: not yet implemented
404    }
405
406    @Override
407    public void setTargetIdsForSource(String sourceId, List<String> targetIds, Session session) {
408        // TODO: not yet implemented
409    }
410
411    @Override
412    public void removeLinksForTarget(String targetId, Session session) {
413        // TODO: not yet implemented
414    }
415
416    @Override
417    public void removeLinksForSource(String sourceId, Session session) {
418        // TODO: not yet implemented
419    }
420
421    @Override
422    // to build helpful debug logs
423    public String toString() {
424        return String.format(
425                "LDAPTreeReference to resolve field='%s' of sourceDirectory='%s'" + " with targetDirectory='%s'",
426                fieldName, sourceDirectoryName, targetDirectoryName);
427    }
428
429    /**
430     * @since 5.6
431     */
432    @Override
433    public LDAPTreeReference clone() {
434        try {
435            // basic fields are already copied by super.clone()
436            return (LDAPTreeReference) super.clone();
437        } catch (CloneNotSupportedException e) {
438            throw new AssertionError(e);
439        }
440    }
441
442}