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