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