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