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