001/*
002 * (C) Copyright 2006-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 *     Nuxeo - initial API and implementation
018 *
019 */
020
021package org.nuxeo.ecm.directory.ldap;
022
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.List;
026import java.util.Set;
027import java.util.TreeSet;
028
029import javax.naming.CompositeName;
030import javax.naming.InvalidNameException;
031import javax.naming.Name;
032import javax.naming.NamingEnumeration;
033import javax.naming.NamingException;
034import javax.naming.directory.Attribute;
035import javax.naming.directory.Attributes;
036import javax.naming.directory.BasicAttribute;
037import javax.naming.directory.BasicAttributes;
038import javax.naming.directory.DirContext;
039import javax.naming.directory.SchemaViolationException;
040import javax.naming.directory.SearchControls;
041import javax.naming.directory.SearchResult;
042import javax.naming.ldap.LdapName;
043import javax.naming.ldap.Rdn;
044
045import org.apache.commons.lang3.StringUtils;
046import org.apache.commons.logging.Log;
047import org.apache.commons.logging.LogFactory;
048import org.apache.directory.shared.ldap.codec.util.LdapURL;
049import org.apache.directory.shared.ldap.codec.util.LdapURLEncodingException;
050import org.nuxeo.common.xmap.annotation.XNode;
051import org.nuxeo.common.xmap.annotation.XNodeList;
052import org.nuxeo.common.xmap.annotation.XObject;
053import org.nuxeo.ecm.core.api.DocumentModel;
054import org.nuxeo.ecm.core.api.PropertyException;
055import org.nuxeo.ecm.directory.AbstractReference;
056import org.nuxeo.ecm.directory.BaseSession;
057import org.nuxeo.ecm.directory.Directory;
058import org.nuxeo.ecm.directory.DirectoryEntryNotFoundException;
059import org.nuxeo.ecm.directory.DirectoryException;
060import org.nuxeo.ecm.directory.DirectoryFieldMapper;
061import org.nuxeo.ecm.directory.ReferenceDescriptor;
062import org.nuxeo.ecm.directory.Session;
063import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector;
064import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector.FilterJobs;
065
066/**
067 * Implementation of the directory Reference interface that leverage two common ways of storing relationships in LDAP
068 * directories:
069 * <ul>
070 * <li>the static attribute strategy where a multi-valued attribute store the exhaustive list of distinguished names of
071 * the refereed entries (eg. the uniqueMember attribute of the groupOfUniqueNames objectclass)</li>
072 * <li>the dynamic attribute strategy where a potentially multi-valued attribute stores a ldap urls intensively
073 * describing the refereed LDAP entries (eg. the memberURLs attribute of the groupOfURLs objectclass)</li>
074 * </ul>
075 * <p>
076 * Please note that both static and dynamic references are resolved in read mode whereas only the static attribute
077 * strategy is used when creating new references or when deleting existing ones (write / update mode).
078 * <p>
079 * Some design considerations behind the implementation of such reference can be found at:
080 * http://jira.nuxeo.org/browse/NXP-1506
081 *
082 * @author Olivier Grisel <ogrisel@nuxeo.com>
083 */
084@XObject(value = "ldapReference")
085public class LDAPReference extends AbstractReference implements Cloneable {
086
087    private static final Log log = LogFactory.getLog(LDAPReference.class);
088
089    @XNodeList(value = "dynamicReference", type = LDAPDynamicReferenceDescriptor[].class, componentType = LDAPDynamicReferenceDescriptor.class)
090    private LDAPDynamicReferenceDescriptor[] dynamicReferences;
091
092    @XNode("@forceDnConsistencyCheck")
093    public boolean forceDnConsistencyCheck;
094
095    protected LDAPDirectoryDescriptor targetDirectoryDescriptor;
096
097    /**
098     * Resolve staticAttributeId as distinguished names (true by default) such as in the uniqueMember field of
099     * groupOfUniqueNames. Set to false to resolve as simple id (as in memberUID of posixGroup for instance).
100     */
101    @XNode("@staticAttributeIdIsDn")
102    private boolean staticAttributeIdIsDn = true;
103
104    @XNode("@staticAttributeId")
105    protected String staticAttributeId;
106
107    @XNode("@dynamicAttributeId")
108    protected String dynamicAttributeId;
109
110    public LDAPReference() {
111        super(null, null);
112    }
113
114    public LDAPReference(ReferenceDescriptor referenceDescriptor) {
115        super(referenceDescriptor.getFieldName(), referenceDescriptor.getDirectory());
116    }
117
118    @XNode("@field")
119    public void setFieldName(String fieldName) {
120        this.fieldName = fieldName;
121    }
122
123    private LDAPFilterMatcher getFilterMatcher() {
124        return new LDAPFilterMatcher();
125    }
126
127    /**
128     * @return true if the reference should resolve statically refereed entries (identified by dn-valued attribute)
129     */
130    public boolean isStatic() throws DirectoryException {
131        return getStaticAttributeId() != null;
132    }
133
134    public String getStaticAttributeId() throws DirectoryException {
135        return getStaticAttributeId(null);
136    }
137
138    public String getStaticAttributeId(DirectoryFieldMapper sourceFM) throws DirectoryException {
139        if (staticAttributeId != null) {
140            // explicitly provided attributeId
141            return staticAttributeId;
142        }
143
144        // sourceFM can be passed to avoid infinite loop in LDAPDirectory init
145        if (sourceFM == null) {
146            sourceFM = ((LDAPDirectory) getSourceDirectory()).getFieldMapper();
147        }
148        String backendFieldId = sourceFM.getBackendField(fieldName);
149        if (fieldName.equals(backendFieldId)) {
150            // no specific backendField found and no staticAttributeId found
151            // either, this reference should not be statically resolved
152            return null;
153        } else {
154            // BBB: the field mapper has been explicitly used to specify the
155            // staticAttributeId value as this was the case before the
156            // introduction of the staticAttributeId dynamicAttributeId duality
157            log.warn(String.format(
158                    "implicit static attribute definition through fieldMapping is deprecated, "
159                            + "please update your setup with "
160                            + "<ldapReference field=\"%s\" directory=\"%s\" staticAttributeId=\"%s\">",
161                    fieldName, sourceDirectoryName, backendFieldId));
162            return backendFieldId;
163        }
164    }
165
166    public List<LDAPDynamicReferenceDescriptor> getDynamicAttributes() {
167        return Arrays.asList(dynamicReferences);
168    }
169
170    public String getDynamicAttributeId() {
171        return dynamicAttributeId;
172    }
173
174    /**
175     * @return true if the reference should resolve dynamically refereed entries (identified by a LDAP url-valued
176     *         attribute)
177     */
178    public boolean isDynamic() {
179        return dynamicAttributeId != null;
180    }
181
182    @Override
183    @XNode("@directory")
184    public void setTargetDirectoryName(String targetDirectoryName) {
185        this.targetDirectoryName = targetDirectoryName;
186    }
187
188    @Override
189    public Directory getSourceDirectory() throws DirectoryException {
190
191        Directory sourceDir = super.getSourceDirectory();
192        if (sourceDir instanceof LDAPDirectory) {
193            return sourceDir;
194        } else {
195            throw new DirectoryException(sourceDirectoryName
196                    + " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName);
197        }
198    }
199
200    @Override
201    public Directory getTargetDirectory() throws DirectoryException {
202        Directory targetDir = super.getTargetDirectory();
203        if (targetDir instanceof LDAPDirectory) {
204            return targetDir;
205        } else {
206            throw new DirectoryException(targetDirectoryName
207                    + " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName);
208        }
209    }
210
211    protected LDAPDirectory getTargetLDAPDirectory() throws DirectoryException {
212        return (LDAPDirectory) getTargetDirectory();
213    }
214
215    protected LDAPDirectory getSourceLDAPDirectory() throws DirectoryException {
216        return (LDAPDirectory) getSourceDirectory();
217    }
218
219    protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() throws DirectoryException {
220        if (targetDirectoryDescriptor == null) {
221            targetDirectoryDescriptor = getTargetLDAPDirectory().getDescriptor();
222        }
223        return targetDirectoryDescriptor;
224    }
225
226    /**
227     * Store new links using the LDAP staticAttributeId strategy.
228     *
229     * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List)
230     */
231    @Override
232    public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException {
233
234        if (targetIds.isEmpty()) {
235            // optim: nothing to do, return silently without further creating
236            // session instances
237            return;
238        }
239
240        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
241        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
242        String attributeId = getStaticAttributeId();
243        if (attributeId == null) {
244            if (log.isTraceEnabled()) {
245                log.trace(String.format("trying to edit a non-static reference from %s in directory %s: ignoring",
246                        sourceId, ldapSourceDirectory.getName()));
247            }
248            return;
249        }
250        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
251                LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
252            // fetch the entry to be able to run the security policy
253            // implemented in an entry adaptor
254            DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
255            if (sourceEntry == null) {
256                throw new DirectoryException(String.format("could not add links from unexisting %s in directory %s",
257                        sourceId, ldapSourceDirectory.getName()));
258            }
259            if (!BaseSession.isReadOnlyEntry(sourceEntry)) {
260                SearchResult ldapEntry = sourceSession.getLdapEntry(sourceId);
261
262                String sourceDn = ldapEntry.getNameInNamespace();
263                Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
264                String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
265                Attribute attrToAdd = new BasicAttribute(attributeId);
266                for (String targetId : targetIds) {
267                    if (staticAttributeIdIsDn) {
268                        // TODO optim: avoid LDAP search request when targetDn
269                        // can be forged client side (rdnAttribute = idAttribute and scope is onelevel)
270                        ldapEntry = targetSession.getLdapEntry(targetId);
271                        if (ldapEntry == null) {
272                            log.warn(String.format(
273                                    "entry '%s' in directory '%s' not found: could not add link from '%s' in directory '%s' for '%s'",
274                                    targetId, ldapTargetDirectory.getName(), sourceId, ldapSourceDirectory.getName(),
275                                    this));
276                            continue;
277                        }
278                        String dn = ldapEntry.getNameInNamespace();
279                        if (storedAttr == null || !storedAttr.contains(dn)) {
280                            attrToAdd.add(dn);
281                        }
282                    } else {
283                        if (storedAttr == null || !storedAttr.contains(targetId)) {
284                            attrToAdd.add(targetId);
285                        }
286                    }
287                }
288                if (attrToAdd.size() > 0) {
289                    try {
290                        // do the LDAP request to store missing dns
291                        Attributes attrsToAdd = new BasicAttributes();
292                        attrsToAdd.put(attrToAdd);
293
294                        if (log.isDebugEnabled()) {
295                            log.debug(String.format(
296                                    "LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s' "
297                                            + "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]",
298                                    sourceId, StringUtils.join(targetIds, ", "), sourceDn, attrsToAdd, this));
299                        }
300                        sourceSession.getContext().modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrsToAdd);
301
302                        // robustly clean any existing empty marker now that we are sure that the list in not empty
303                        if (storedAttr.contains(emptyRefMarker)) {
304                            Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);
305
306                            if (log.isDebugEnabled()) {
307                                log.debug(String.format(
308                                        "LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s'"
309                                                + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
310                                        sourceId, StringUtils.join(targetIds, ", "), sourceDn, cleanAttrs, this));
311                            }
312                            sourceSession.getContext().modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE,
313                                    cleanAttrs);
314                        }
315                    } catch (SchemaViolationException e) {
316                        if (isDynamic()) {
317                            // we are editing an entry that has no static part
318                            log.warn(String.format("cannot update dynamic reference in field %s for source %s",
319                                    getFieldName(), sourceId));
320                        } else {
321                            // this is a real schema configuration problem,
322                            // wrap up the exception
323                            throw new DirectoryException(e);
324                        }
325                    }
326                }
327            }
328        } catch (NamingException e) {
329            throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
330        }
331    }
332
333    /**
334     * Store new links using the LDAP staticAttributeId strategy.
335     *
336     * @see org.nuxeo.ecm.directory.Reference#addLinks(List, String)
337     */
338    @Override
339    public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException {
340        String attributeId = getStaticAttributeId();
341        if (attributeId == null && !sourceIds.isEmpty()) {
342            log.warn("trying to edit a non-static reference: ignoring");
343            return;
344        }
345        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
346        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
347
348        String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
349        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
350                LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
351            if (!sourceSession.isReadOnly()) {
352                // compute the target dn to add to all the matching source
353                // entries
354                SearchResult ldapEntry = targetSession.getLdapEntry(targetId);
355                if (ldapEntry == null) {
356                    throw new DirectoryException(String.format("could not add links to unexisting %s in directory %s",
357                            targetId, ldapTargetDirectory.getName()));
358                }
359                String targetAttributeValue;
360                if (staticAttributeIdIsDn) {
361                    targetAttributeValue = ldapEntry.getNameInNamespace();
362                } else {
363                    targetAttributeValue = targetId;
364                }
365
366                for (String sourceId : sourceIds) {
367                    // fetch the entry to be able to run the security policy
368                    // implemented in an entry adaptor
369                    DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
370                    if (sourceEntry == null) {
371                        log.warn(String.format(
372                                "entry %s in directory %s not found: could not add link to %s in directory %s",
373                                sourceId, ldapSourceDirectory.getName(), targetId, ldapTargetDirectory.getName()));
374                        continue;
375                    }
376                    if (BaseSession.isReadOnlyEntry(sourceEntry)) {
377                        // skip this entry since it cannot be edited to add the
378                        // reference to targetId
379                        log.warn(String.format(
380                                "entry %s in directory %s is readonly: could not add link to %s in directory %s",
381                                sourceId, ldapSourceDirectory.getName(), targetId, ldapTargetDirectory.getName()));
382                        continue;
383                    }
384                    ldapEntry = sourceSession.getLdapEntry(sourceId);
385                    String sourceDn = ldapEntry.getNameInNamespace();
386                    Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
387                    if (storedAttr.contains(targetAttributeValue)) {
388                        // no need to readd
389                        continue;
390                    }
391                    try {
392                        // add the new dn
393                        Attributes attrs = new BasicAttributes(attributeId, targetAttributeValue);
394
395                        if (log.isDebugEnabled()) {
396                            log.debug(String.format(
397                                    "LDAPReference.addLinks([%s], %s): LDAP modifyAttributes dn='%s'"
398                                            + " mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]",
399                                    StringUtils.join(sourceIds, ", "), targetId, sourceDn, attrs, this));
400                        }
401                        sourceSession.getContext().modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrs);
402
403                        // robustly clean any existing empty marker now that we
404                        // are sure that the list in not empty
405                        if (storedAttr.contains(emptyRefMarker)) {
406                            Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);
407                            if (log.isDebugEnabled()) {
408                                log.debug(String.format(
409                                        "LDAPReference.addLinks(%s, %s): LDAP modifyAttributes dn='%s'"
410                                                + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
411                                        StringUtils.join(sourceIds, ", "), targetId, sourceDn, cleanAttrs.toString(),
412                                        this));
413                            }
414                            sourceSession.getContext().modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE,
415                                    cleanAttrs);
416                        }
417                    } catch (SchemaViolationException e) {
418                        if (isDynamic()) {
419                            // we are editing an entry that has no static part
420                            log.warn(String.format("cannot add dynamic reference in field %s for target %s",
421                                    getFieldName(), targetId));
422                        } else {
423                            // this is a real schema configuration problem,
424                            // wrap the exception
425                            throw new DirectoryException(e);
426                        }
427                    }
428                }
429            }
430        } catch (NamingException e) {
431            throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
432        }
433    }
434
435    /**
436     * Fetch both statically and dynamically defined references and merge the results.
437     *
438     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
439     */
440    @Override
441    public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException {
442
443        // container to hold merged references
444        Set<String> sourceIds = new TreeSet<>();
445        SearchResult targetLdapEntry = null;
446        String targetDn = null;
447
448        // fetch all attributes when dynamic groups are used
449        boolean fetchAllAttributes = isDynamic();
450
451        // step #1: resolve static references
452        String staticAttributeId = getStaticAttributeId();
453        if (staticAttributeId != null) {
454            // step #1.1: fetch the dn of the targetId entry in the target
455            // directory by the static dn valued strategy
456            LDAPDirectory targetDir = getTargetLDAPDirectory();
457
458            if (staticAttributeIdIsDn) {
459                try (LDAPSession targetSession = (LDAPSession) targetDir.getSession()) {
460                    targetLdapEntry = targetSession.getLdapEntry(targetId, fetchAllAttributes);
461                    if (targetLdapEntry == null) {
462                        String msg = String.format(
463                                "Failed to perform inverse lookup on LDAPReference"
464                                        + " resolving field '%s' of '%s' to entries of '%s'"
465                                        + " using the static content of attribute '%s':"
466                                        + " entry '%s' cannot be found in '%s'",
467                                fieldName, sourceDirectory, targetDirectoryName, staticAttributeId, targetId,
468                                targetDirectoryName);
469                        throw new DirectoryEntryNotFoundException(msg);
470                    }
471                    targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
472
473                } catch (NamingException e) {
474                    throw new DirectoryException(
475                            "error fetching " + targetId + " from " + targetDirectoryName + ": " + e.getMessage(), e);
476                }
477            }
478
479            // step #1.2: search for entries that reference that dn in the
480            // source directory and collect their ids
481            LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory();
482
483            String filterExpr = String.format("(&(%s={0})%s)", staticAttributeId, ldapSourceDirectory.getBaseFilter());
484            String[] filterArgs = new String[1];
485
486            if (staticAttributeIdIsDn) {
487                filterArgs[0] = targetDn;
488            } else {
489                filterArgs[0] = targetId;
490            }
491
492            String searchBaseDn = ldapSourceDirectory.getDescriptor().getSearchBaseDn();
493            SearchControls sctls = ldapSourceDirectory.getSearchControls();
494            try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
495                if (log.isDebugEnabled()) {
496                    log.debug(String.format(
497                            "LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
498                                    + " filter='%s' args='%s' scope='%s' [%s]",
499                            targetId, searchBaseDn, filterExpr, StringUtils.join(filterArgs, ", "),
500                            sctls.getSearchScope(), this));
501                }
502                NamingEnumeration<SearchResult> results = sourceSession.getContext().search(searchBaseDn, filterExpr,
503                        filterArgs, sctls);
504
505                try {
506                    while (results.hasMore()) {
507                        Attributes attributes = results.next().getAttributes();
508                        // NXP-2461: check that id field is filled
509                        Attribute attr = attributes.get(sourceSession.idAttribute);
510                        if (attr != null) {
511                            Object value = attr.get();
512                            if (value != null) {
513                                sourceIds.add(value.toString());
514                            }
515                        }
516                    }
517                } finally {
518                    results.close();
519                }
520            } catch (NamingException e) {
521                throw new DirectoryException("error during reference search for " + filterArgs[0], e);
522            }
523        }
524        // step #2: resolve dynamic references
525        String dynamicAttributeId = this.dynamicAttributeId;
526        if (dynamicAttributeId != null) {
527
528            LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory();
529            LDAPDirectory ldapTargetDirectory = getTargetLDAPDirectory();
530            String searchBaseDn = ldapSourceDirectory.getDescriptor().getSearchBaseDn();
531
532            try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession();
533                    LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
534                // step #2.1: fetch the target entry to apply the ldap url
535                // filters of the candidate sources on it
536                if (targetLdapEntry == null) {
537                    // only fetch the entry if not already fetched by the
538                    // static
539                    // attributes references resolution
540                    targetLdapEntry = targetSession.getLdapEntry(targetId, fetchAllAttributes);
541                }
542                if (targetLdapEntry == null) {
543                    String msg = String.format(
544                            "Failed to perform inverse lookup on LDAPReference"
545                                    + " resolving field '%s' of '%s' to entries of '%s'"
546                                    + " using the dynamic content of attribute '%s':"
547                                    + " entry '%s' cannot be found in '%s'",
548                            fieldName, ldapSourceDirectory, targetDirectoryName, dynamicAttributeId, targetId,
549                            targetDirectoryName);
550                    throw new DirectoryException(msg);
551                }
552                targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
553                Attributes targetAttributes = targetLdapEntry.getAttributes();
554
555                // step #2.2: find the list of entries that hold candidate
556                // dynamic links in the source directory
557                SearchControls sctls = new SearchControls();
558                sctls.setSearchScope(ldapSourceDirectory.getDescriptor().getSearchScope());
559                sctls.setReturningAttributes(new String[] { sourceSession.idAttribute, dynamicAttributeId });
560                String filterExpr = String.format("%s=*", dynamicAttributeId);
561
562                if (log.isDebugEnabled()) {
563                    log.debug(String.format(
564                            "LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
565                                    + " filter='%s' scope='%s' [%s]",
566                            targetId, searchBaseDn, filterExpr, sctls.getSearchScope(), this));
567                }
568                NamingEnumeration<SearchResult> results = sourceSession.getContext().search(searchBaseDn, filterExpr,
569                        sctls);
570                try {
571                    while (results.hasMore()) {
572                        // step #2.3: for each sourceId and each ldapUrl test
573                        // whether the current target entry matches the
574                        // collected
575                        // URL
576                        Attributes sourceAttributes = results.next().getAttributes();
577
578                        NamingEnumeration<?> ldapUrls = sourceAttributes.get(dynamicAttributeId).getAll();
579                        try {
580                            while (ldapUrls.hasMore()) {
581                                LdapURL ldapUrl = new LdapURL(ldapUrls.next().toString());
582                                String candidateDN = pseudoNormalizeDn(ldapUrl.getDn().getUpName());
583                                // check base URL
584                                if (!targetDn.endsWith(candidateDN)) {
585                                    continue;
586                                }
587
588                                // check onelevel scope constraints
589                                if (ldapUrl.getScope() == SearchControls.ONELEVEL_SCOPE) {
590                                    int targetDnSize = new LdapName(targetDn).size();
591                                    int urlDnSize = new LdapName(candidateDN).size();
592                                    if (targetDnSize - urlDnSize > 1) {
593                                        // target is not a direct child of the
594                                        // DN of the
595                                        // LDAP URL
596                                        continue;
597                                    }
598                                }
599
600                                // check that the target entry matches the
601                                // filter
602                                if (getFilterMatcher().match(targetAttributes, ldapUrl.getFilter())) {
603                                    // the target match the source url, add it
604                                    // to the
605                                    // collected ids
606                                    sourceIds.add(sourceAttributes.get(sourceSession.idAttribute).get().toString());
607                                }
608                            }
609                        } finally {
610                            ldapUrls.close();
611                        }
612                    }
613                } finally {
614                    results.close();
615                }
616            } catch (NamingException | LdapURLEncodingException e) {
617                throw new DirectoryException("error during reference search for " + targetId, e);
618            }
619        }
620
621        /*
622         * This kind of reference is not supported because Active Directory use filter expression not yet supported by
623         * LDAPFilterMatcher. See NXP-4562
624         */
625        if (dynamicReferences != null && dynamicReferences.length > 0) {
626            log.error("This kind of reference is not supported.");
627        }
628
629        return new ArrayList<>(sourceIds);
630    }
631
632    /**
633     * Fetches both statically and dynamically defined references and merges the results.
634     *
635     * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
636     */
637    @Override
638    // XXX: broken, use getLdapTargetIds for a proper implementation
639    @SuppressWarnings("unchecked")
640    public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException {
641        String schemaName = getSourceDirectory().getSchema();
642        try (Session session = getSourceDirectory().getSession()) {
643            try {
644                return (List<String>) session.getEntry(sourceId).getProperty(schemaName, fieldName);
645            } catch (PropertyException e) {
646                throw new DirectoryException(e);
647            }
648        }
649    }
650
651    /**
652     * Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for
653     * comparison purpose.
654     *
655     * @param dn the raw unnormalized dn
656     * @return lowercase version without whitespace after commas
657     */
658    protected static String pseudoNormalizeDn(String dn) throws InvalidNameException {
659        LdapName ldapName = new LdapName(dn);
660        List<String> rdns = new ArrayList<>();
661        for (Rdn rdn : ldapName.getRdns()) {
662            String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,");
663            String rdnStr = rdn.getType().toLowerCase() + "=" + value;
664            rdns.add(0, rdnStr);
665        }
666        return StringUtils.join(rdns, ',');
667    }
668
669    /**
670     * Optimized method to spare a LDAP request when the caller is a LDAPSession object that has already fetched the
671     * LDAP Attribute instances.
672     * <p>
673     * This method should return the same results as the sister method: org.nuxeo
674     * .ecm.directory.Reference#getTargetIdsForSource(java.lang.String)
675     *
676     * @return target reference ids
677     */
678    public List<String> getLdapTargetIds(Attributes attributes) throws DirectoryException {
679
680        Set<String> targetIds = new TreeSet<>();
681
682        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
683        LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
684        String emptyRefMarker = ldapTargetDirectory.getDescriptor().getEmptyRefMarker();
685        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
686            String baseDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
687
688            // step #1: fetch ids referenced by static attributes
689            String staticAttributeId = getStaticAttributeId();
690            Attribute staticAttribute = null;
691            if (staticAttributeId != null) {
692                staticAttribute = attributes.get(staticAttributeId);
693            }
694
695            if (staticAttribute != null && !staticAttributeIdIsDn) {
696                NamingEnumeration<?> staticContent = staticAttribute.getAll();
697                try {
698                    while (staticContent.hasMore()) {
699                        String value = staticContent.next().toString();
700                        if (!emptyRefMarker.equals(value)) {
701                            targetIds.add(value);
702                        }
703                    }
704                } finally {
705                    staticContent.close();
706                }
707            }
708
709            if (staticAttribute != null && staticAttributeIdIsDn) {
710                NamingEnumeration<?> targetDns = staticAttribute.getAll();
711                try {
712                    while (targetDns.hasMore()) {
713                        String targetDn = targetDns.next().toString();
714
715                        if (!pseudoNormalizeDn(targetDn).endsWith(baseDn)) {
716                            // optim: avoid network connections when obvious
717                            if (log.isTraceEnabled()) {
718                                log.trace(String.format("ignoring: dn='%s' (does not match '%s') for '%s'", targetDn,
719                                        baseDn, this));
720                            }
721                            continue;
722                        }
723                        // find the id of the referenced entry
724                        String id;
725
726                        if (targetSession.rdnMatchesIdField()) {
727                            // optim: do not fetch the entry to get its true id
728                            // but
729                            // guess it by reading the targetDn
730                            LdapName name = new LdapName(targetDn);
731                            String rdn = name.get(name.size() - 1);
732                            int pos = rdn.indexOf("=");
733                            id = rdn.substring(pos + 1);
734                        } else {
735                            id = getIdForDn(targetSession, targetDn);
736                            if (id == null) {
737                                log.warn(String.format(
738                                        "ignoring target '%s' (missing attribute '%s') while resolving reference '%s'",
739                                        targetDn, targetSession.idAttribute, this));
740                                continue;
741                            }
742                        }
743                        if (forceDnConsistencyCheck) {
744                            // check that the referenced entry is actually part
745                            // of
746                            // the target directory (takes care of the filters
747                            // and
748                            // the scope)
749                            // this check can be very expensive on large groups
750                            // and thus not enabled by default
751                            if (!targetSession.hasEntry(id)) {
752                                if (log.isTraceEnabled()) {
753                                    log.trace(
754                                            String.format(
755                                                    "ignoring target '%s' when resolving '%s' (not part of target"
756                                                            + " directory by forced DN consistency check)",
757                                                    targetDn, this));
758                                }
759                                continue;
760                            }
761                        }
762                        // NXP-2461: check that id field is filled
763                        if (id != null) {
764                            targetIds.add(id);
765                        }
766                    }
767                } finally {
768                    targetDns.close();
769                }
770            }
771            // step #2: fetched dynamically referenced ids
772            String dynamicAttributeId = this.dynamicAttributeId;
773            Attribute dynamicAttribute = null;
774            if (dynamicAttributeId != null) {
775                dynamicAttribute = attributes.get(dynamicAttributeId);
776            }
777            if (dynamicAttribute != null) {
778                NamingEnumeration<?> rawldapUrls = dynamicAttribute.getAll();
779                try {
780                    while (rawldapUrls.hasMore()) {
781                        LdapURL ldapUrl = new LdapURL(rawldapUrls.next().toString());
782                        String linkDn = pseudoNormalizeDn(ldapUrl.getDn().getUpName());
783                        String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
784                        int scope = SearchControls.ONELEVEL_SCOPE;
785                        int scopePart = ldapUrl.getScope();
786                        if (scopePart == SearchControls.SUBTREE_SCOPE) {
787                            scope = SearchControls.SUBTREE_SCOPE;
788                        }
789                        if (!linkDn.endsWith(directoryDn) && !directoryDn.endsWith(linkDn)) {
790                            // optim #1: if the dns do not match, abort
791                            continue;
792                        } else if (directoryDn.endsWith(linkDn) && linkDn.length() < directoryDn.length()
793                                && scope == SearchControls.ONELEVEL_SCOPE) {
794                            // optim #2: the link dn is pointing to elements
795                            // that at
796                            // upperlevel than directory elements
797                            continue;
798                        } else {
799
800                            // Search for references elements
801                            targetIds.addAll(
802                                    getReferencedElements(attributes, directoryDn, linkDn, ldapUrl.getFilter(), scope));
803
804                        }
805                    }
806                } finally {
807                    rawldapUrls.close();
808                }
809            }
810
811            if (dynamicReferences != null && dynamicReferences.length > 0) {
812
813                // Only the first Dynamic Reference is used
814                LDAPDynamicReferenceDescriptor dynAtt = dynamicReferences[0];
815
816                Attribute baseDnsAttribute = attributes.get(dynAtt.baseDN);
817                Attribute filterAttribute = attributes.get(dynAtt.filter);
818
819                if (baseDnsAttribute != null && filterAttribute != null) {
820
821                    NamingEnumeration<?> baseDns = null;
822                    NamingEnumeration<?> filters = null;
823
824                    try {
825                        // Get the BaseDN value from the descriptor
826                        baseDns = baseDnsAttribute.getAll();
827                        String linkDnValue = baseDns.next().toString();
828                        baseDns.close();
829                        linkDnValue = pseudoNormalizeDn(linkDnValue);
830
831                        // Get the filter value from the descriptor
832                        filters = filterAttribute.getAll();
833                        String filterValue = filters.next().toString();
834                        filters.close();
835
836                        // Get the scope value from the descriptor
837                        int scope = "subtree".equalsIgnoreCase(dynAtt.type) ? SearchControls.SUBTREE_SCOPE
838                                : SearchControls.ONELEVEL_SCOPE;
839
840                        String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
841
842                        // if the dns match, and if the link dn is pointing to
843                        // elements that at upperlevel than directory elements
844                        if ((linkDnValue.endsWith(directoryDn) || directoryDn.endsWith(linkDnValue))
845                                && !(directoryDn.endsWith(linkDnValue) && linkDnValue.length() < directoryDn.length()
846                                        && scope == SearchControls.ONELEVEL_SCOPE)) {
847
848                            // Correct the filter expression
849                            filterValue = FilterExpressionCorrector.correctFilter(filterValue, FilterJobs.CORRECT_NOT);
850
851                            // Search for references elements
852                            targetIds.addAll(
853                                    getReferencedElements(attributes, directoryDn, linkDnValue, filterValue, scope));
854
855                        }
856                    } finally {
857                        if (baseDns != null) {
858                            baseDns.close();
859                        }
860
861                        if (filters != null) {
862                            filters.close();
863                        }
864                    }
865
866                }
867
868            }
869            // return merged attributes
870            return new ArrayList<>(targetIds);
871        } catch (NamingException | LdapURLEncodingException e) {
872            throw new DirectoryException("error computing LDAP references", e);
873        }
874    }
875
876    protected String getIdForDn(LDAPSession session, String dn) {
877        // the entry id is not based on the rdn, we thus need to
878        // fetch the LDAP entry to grab it
879        String[] attributeIdsToCollect = { session.idAttribute };
880        Attributes entry;
881        try {
882
883            if (log.isDebugEnabled()) {
884                log.debug(String.format(
885                        "LDAPReference.getIdForDn(session, %s): LDAP get dn='%s'"
886                                + " attribute ids to collect='%s' [%s]",
887                        dn, dn, StringUtils.join(attributeIdsToCollect, ", "), this));
888            }
889
890            Name name = new CompositeName().add(dn);
891            entry = session.getContext().getAttributes(name, attributeIdsToCollect);
892        } catch (NamingException e) {
893            return null;
894        }
895        // NXP-2461: check that id field is filled
896        Attribute attr = entry.get(session.idAttribute);
897        if (attr != null) {
898            try {
899                return attr.get().toString();
900            } catch (NamingException e) {
901            }
902        }
903        return null;
904    }
905
906    /**
907     * Retrieve the elements referenced by the filter/BaseDN/Scope request.
908     *
909     * @param attributes Attributes of the referencer element
910     * @param directoryDn Dn of the Directory
911     * @param linkDn Dn specified in the parent
912     * @param filter Filter expression specified in the parent
913     * @param scope scope for the search
914     * @return The list of the referenced elements.
915     */
916    private Set<String> getReferencedElements(Attributes attributes, String directoryDn, String linkDn, String filter,
917            int scope) throws DirectoryException, NamingException {
918
919        Set<String> targetIds = new TreeSet<>();
920
921        LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
922        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
923        LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
924
925        // use the most specific scope between the one specified in the
926        // Directory and the specified in the Parent
927        String dn = directoryDn.endsWith(linkDn) && directoryDn.length() > linkDn.length() ? directoryDn : linkDn;
928
929        // combine the ldapUrl search query with target
930        // directory own constraints
931        SearchControls scts = new SearchControls();
932
933        // use the most specific scope
934        scts.setSearchScope(Math.min(scope, targetDirconfig.getSearchScope()));
935
936        // only fetch the ids of the targets
937        scts.setReturningAttributes(new String[] { targetSession.idAttribute });
938
939        // combine the filter of the target directory with the
940        // provided filter if any
941        String targetFilter = targetDirconfig.getSearchFilter();
942        if (filter == null || filter.length() == 0) {
943            filter = targetFilter;
944        } else if (targetFilter != null && targetFilter.length() > 0) {
945            filter = String.format("(&(%s)(%s))", targetFilter, filter);
946        }
947
948        // perform the request and collect the ids
949        if (log.isDebugEnabled()) {
950            log.debug(String.format(
951                    "LDAPReference.getLdapTargetIds(%s): LDAP search dn='%s' " + " filter='%s' scope='%s' [%s]",
952                    attributes, dn, dn, scts.getSearchScope(), this));
953        }
954
955        Name name = new CompositeName().add(dn);
956        NamingEnumeration<SearchResult> results = targetSession.getContext().search(name, filter, scts);
957        try {
958            while (results.hasMore()) {
959                // NXP-2461: check that id field is filled
960                Attribute attr = results.next().getAttributes().get(targetSession.idAttribute);
961                if (attr != null) {
962                    String collectedId = attr.get().toString();
963                    if (collectedId != null) {
964                        targetIds.add(collectedId);
965                    }
966                }
967
968            }
969        } finally {
970            results.close();
971        }
972
973        return targetIds;
974    }
975
976    /**
977     * Remove existing statically defined links for the given source id (dynamic references remain unaltered)
978     *
979     * @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String)
980     */
981    @Override
982    public void removeLinksForSource(String sourceId) throws DirectoryException {
983        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
984        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
985        String attributeId = getStaticAttributeId();
986        try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession();
987                LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
988            if (sourceSession.isReadOnly() || attributeId == null) {
989                // do not try to do anything on a read only server or to a
990                // purely dynamic reference
991                return;
992            }
993            // get the dn of the entry that matches sourceId
994            SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId);
995            if (sourceLdapEntry == null) {
996                throw new DirectoryException(
997                        String.format("cannot edit the links hold by missing entry '%s' in directory '%s'", sourceId,
998                                ldapSourceDirectory.getName()));
999            }
1000            String sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace());
1001
1002            Attribute oldAttr = sourceLdapEntry.getAttributes().get(attributeId);
1003            if (oldAttr == null) {
1004                // consider it as an empty attribute to simplify the following
1005                // code
1006                oldAttr = new BasicAttribute(attributeId);
1007            }
1008            Attribute attrToRemove = new BasicAttribute(attributeId);
1009
1010            NamingEnumeration<?> oldAttrs = oldAttr.getAll();
1011            String targetBaseDn = pseudoNormalizeDn(ldapTargetDirectory.getDescriptor().getSearchBaseDn());
1012            try {
1013                while (oldAttrs.hasMore()) {
1014                    String targetKeyAttr = oldAttrs.next().toString();
1015
1016                    if (staticAttributeIdIsDn) {
1017                        String dn = pseudoNormalizeDn(targetKeyAttr);
1018                        if (forceDnConsistencyCheck) {
1019                            String id = getIdForDn(targetSession, dn);
1020                            if (id != null && targetSession.hasEntry(id)) {
1021                                // this is an entry managed by the current
1022                                // reference
1023                                attrToRemove.add(dn);
1024                            }
1025                        } else if (dn.endsWith(targetBaseDn)) {
1026                            // this is an entry managed by the current
1027                            // reference
1028                            attrToRemove.add(dn);
1029                        }
1030                    } else {
1031                        attrToRemove.add(targetKeyAttr);
1032                    }
1033                }
1034            } finally {
1035                oldAttrs.close();
1036            }
1037            try {
1038                if (attrToRemove.size() == oldAttr.size()) {
1039                    // use the empty ref marker to avoid empty attr
1040                    String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
1041                    Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);
1042                    if (log.isDebugEnabled()) {
1043                        log.debug(String.format(
1044                                "LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes key='%s' "
1045                                        + " mod_op='REPLACE_ATTRIBUTE' attrs='%s' [%s]",
1046                                sourceId, sourceDn, emptyAttribute, this));
1047                    }
1048                    sourceSession.getContext().modifyAttributes(sourceDn, DirContext.REPLACE_ATTRIBUTE, emptyAttribute);
1049                } else if (attrToRemove.size() > 0) {
1050                    // remove the attribute managed by the current reference
1051                    Attributes attrsToRemove = new BasicAttributes();
1052                    attrsToRemove.put(attrToRemove);
1053                    if (log.isDebugEnabled()) {
1054                        log.debug(String.format(
1055                                "LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes dn='%s' "
1056                                        + " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
1057                                sourceId, sourceDn, attrsToRemove, this));
1058                    }
1059                    sourceSession.getContext().modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, attrsToRemove);
1060                }
1061            } catch (SchemaViolationException e) {
1062                if (isDynamic()) {
1063                    // we are editing an entry that has no static part
1064                    log.warn(String.format("cannot remove dynamic reference in field %s for source %s", getFieldName(),
1065                            sourceId));
1066                } else {
1067                    // this is a real schma configuration problem, wrapup the
1068                    // exception
1069                    throw new DirectoryException(e);
1070                }
1071            }
1072        } catch (NamingException e) {
1073            throw new DirectoryException("removeLinksForSource failed: " + e.getMessage(), e);
1074        }
1075    }
1076
1077    /**
1078     * Remove existing statically defined links for the given target id (dynamic references remain unaltered)
1079     *
1080     * @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String)
1081     */
1082    @Override
1083    public void removeLinksForTarget(String targetId) throws DirectoryException {
1084        if (!isStatic()) {
1085            // nothing to do: dynamic references cannot be updated
1086            return;
1087        }
1088        LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
1089        LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
1090        String attributeId = getStaticAttributeId();
1091        try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
1092                LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
1093            if (!sourceSession.isReadOnly()) {
1094                // get the dn of the target that matches targetId
1095                String targetAttributeValue;
1096
1097                if (staticAttributeIdIsDn) {
1098                    SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId);
1099                    if (targetLdapEntry == null) {
1100                        String rdnAttribute = ldapTargetDirectory.getDescriptor().getRdnAttribute();
1101                        if (!rdnAttribute.equals(targetSession.idAttribute)) {
1102                            log.warn(String.format(
1103                                    "cannot remove links to missing entry %s in directory %s for reference %s",
1104                                    targetId, ldapTargetDirectory.getName(), this));
1105                            return;
1106                        }
1107                        // the entry might have already been deleted, try to
1108                        // re-forge it if possible (might not work if scope is
1109                        // subtree)
1110                        targetAttributeValue = String.format("%s=%s,%s", rdnAttribute, targetId,
1111                                ldapTargetDirectory.getDescriptor().getSearchBaseDn());
1112                    } else {
1113                        targetAttributeValue = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
1114                    }
1115                } else {
1116                    targetAttributeValue = targetId;
1117                }
1118
1119                // build a LDAP query to find entries that point to the target
1120                String searchFilter = String.format("(%s=%s)", attributeId, targetAttributeValue);
1121                String sourceFilter = ldapSourceDirectory.getBaseFilter();
1122
1123                if (sourceFilter != null && !"".equals(sourceFilter)) {
1124                    searchFilter = String.format("(&(%s)(%s))", searchFilter, sourceFilter);
1125                }
1126
1127                SearchControls scts = new SearchControls();
1128                scts.setSearchScope(ldapSourceDirectory.getDescriptor().getSearchScope());
1129                scts.setReturningAttributes(new String[] { attributeId });
1130
1131                // find all source entries that point to the target key and
1132                // clean
1133                // those references
1134                if (log.isDebugEnabled()) {
1135                    log.debug(String.format(
1136                            "LDAPReference.removeLinksForTarget(%s): LDAP search baseDn='%s' "
1137                                    + " filter='%s' scope='%s' [%s]",
1138                            targetId, sourceSession.searchBaseDn, searchFilter, scts.getSearchScope(), this));
1139                }
1140                NamingEnumeration<SearchResult> results = sourceSession.getContext().search(sourceSession.searchBaseDn,
1141                        searchFilter, scts);
1142                String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
1143                Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);
1144
1145                try {
1146                    while (results.hasMore()) {
1147                        SearchResult result = results.next();
1148                        Attributes attrs = result.getAttributes();
1149                        Attribute attr = attrs.get(attributeId);
1150                        try {
1151                            if (attr.size() == 1) {
1152                                // the attribute holds the last reference, put
1153                                // the
1154                                // empty ref. marker before removing the
1155                                // attribute
1156                                // since empty attribute are often not allowed
1157                                // by
1158                                // the server schema
1159                                if (log.isDebugEnabled()) {
1160                                    log.debug(String.format(
1161                                            "LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
1162                                                    + "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]",
1163                                            targetId, result.getNameInNamespace(), attrs, this));
1164                                }
1165                                sourceSession.getContext().modifyAttributes(result.getNameInNamespace(),
1166                                        DirContext.ADD_ATTRIBUTE, emptyAttribute);
1167                            }
1168                            // remove the reference to the target key
1169                            attrs = new BasicAttributes();
1170                            attr = new BasicAttribute(attributeId);
1171                            attr.add(targetAttributeValue);
1172                            attrs.put(attr);
1173                            if (log.isDebugEnabled()) {
1174                                log.debug(String.format(
1175                                        "LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
1176                                                + "mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
1177                                        targetId, result.getNameInNamespace(), attrs, this));
1178                            }
1179                            sourceSession.getContext().modifyAttributes(result.getNameInNamespace(),
1180                                    DirContext.REMOVE_ATTRIBUTE, attrs);
1181                        } catch (SchemaViolationException e) {
1182                            if (isDynamic()) {
1183                                // we are editing an entry that has no static
1184                                // part
1185                                log.warn(String.format("cannot remove dynamic reference in field %s for target %s",
1186                                        getFieldName(), targetId));
1187                            } else {
1188                                // this is a real schema configuration problem,
1189                                // wrapup the exception
1190                                throw new DirectoryException(e);
1191                            }
1192                        }
1193                    }
1194                } finally {
1195                    results.close();
1196                }
1197            }
1198        } catch (NamingException e) {
1199            throw new DirectoryException("removeLinksForTarget failed: " + e.getMessage(), e);
1200        }
1201    }
1202
1203    /**
1204     * Edit the list of statically defined references for a given target (dynamic references remain unaltered)
1205     *
1206     * @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List)
1207     */
1208    @Override
1209    public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException {
1210        removeLinksForTarget(targetId);
1211        addLinks(sourceIds, targetId);
1212    }
1213
1214    @Override
1215    public void setSourceIdsForTarget(String targetId, List<String> sourceIds, Session session)
1216            throws DirectoryException {
1217        setSourceIdsForTarget(targetId, sourceIds);
1218    }
1219
1220    /**
1221     * Set the list of statically defined references for a given source (dynamic references remain unaltered)
1222     *
1223     * @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List)
1224     */
1225    @Override
1226    public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException {
1227        removeLinksForSource(sourceId);
1228        addLinks(sourceId, targetIds);
1229    }
1230
1231    @Override
1232    public void setTargetIdsForSource(String sourceId, List<String> targetIds, Session session)
1233            throws DirectoryException {
1234        setTargetIdsForSource(sourceId, targetIds);
1235    }
1236
1237    @Override
1238    public void removeLinksForTarget(String targetId, Session session) throws DirectoryException {
1239        removeLinksForTarget(targetId);
1240    }
1241
1242    @Override
1243    public void removeLinksForSource(String sourceId, Session session) throws DirectoryException {
1244        removeLinksForSource(sourceId);
1245    }
1246
1247    @Override
1248    // to build helpful debug logs
1249    public String toString() {
1250        return String.format(
1251                "LDAPReference to resolve field='%s' of sourceDirectory='%s'" + " with targetDirectory='%s'"
1252                        + " and staticAttributeId='%s', dynamicAttributeId='%s'",
1253                fieldName, sourceDirectoryName, targetDirectoryName, staticAttributeId, dynamicAttributeId);
1254    }
1255
1256    /**
1257     * @since 5.6
1258     */
1259    @Override
1260    public LDAPReference clone() {
1261        try {
1262            // basic fields are already copied by super.clone()
1263            return (LDAPReference) super.clone();
1264        } catch (CloneNotSupportedException e) {
1265            throw new AssertionError(e);
1266        }
1267    }
1268
1269    @Override
1270    public void addLinks(String sourceId, List<String> targetIds, Session session) throws DirectoryException {
1271        // TODO to use for optimization
1272        addLinks(sourceId, targetIds);
1273    }
1274
1275    @Override
1276    public void addLinks(List<String> sourceIds, String targetId, Session session) throws DirectoryException {
1277        // TODO to use for optimization
1278        addLinks(sourceIds, targetId);
1279    }
1280
1281}