/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*
*/
package org.nuxeo.ecm.directory.ldap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.naming.CompositeName;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SchemaViolationException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.lang.StringUtils;
import org.nuxeo.common.xmap.annotation.XNode;
import org.nuxeo.common.xmap.annotation.XNodeList;
import org.nuxeo.common.xmap.annotation.XObject;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.directory.AbstractReference;
import org.nuxeo.ecm.directory.BaseSession;
import org.nuxeo.ecm.directory.Directory;
import org.nuxeo.ecm.directory.DirectoryEntryNotFoundException;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.DirectoryFieldMapper;
import org.nuxeo.ecm.directory.Session;
import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector;
import org.nuxeo.ecm.directory.ldap.filter.FilterExpressionCorrector.FilterJobs;
import com.sun.jndi.ldap.LdapURL;
/**
* Implementation of the directory Reference interface that leverage two common ways of storing relationships in LDAP
* directories:
* <ul>
* <li>the static attribute strategy where a multi-valued attribute store the exhaustive list of distinguished names of
* the refereed entries (eg. the uniqueMember attribute of the groupOfUniqueNames objectclass)</li>
* <li>the dynamic attribute strategy where a potentially multi-valued attribute stores a ldap urls intensively
* describing the refereed LDAP entries (eg. the memberURLs attribute of the groupOfURLs objectclass)</li>
* </ul>
* <p>
* Please note that both static and dynamic references are resolved in read mode whereas only the static attribute
* strategy is used when creating new references or when deleting existing ones (write / update mode).
* <p>
* Some design considerations behind the implementation of such reference can be found at:
* http://jira.nuxeo.org/browse/NXP-1506
*
* @author Olivier Grisel <ogrisel@nuxeo.com>
*/
@XObject(value = "ldapReference")
public class LDAPReference extends AbstractReference {
private static final Log log = LogFactory.getLog(LDAPReference.class);
@XNodeList(value = "dynamicReference", type = LDAPDynamicReferenceDescriptor[].class, componentType = LDAPDynamicReferenceDescriptor.class)
private LDAPDynamicReferenceDescriptor[] dynamicReferences;
@XNode("@forceDnConsistencyCheck")
public boolean forceDnConsistencyCheck;
protected LDAPDirectoryDescriptor targetDirectoryDescriptor;
/**
* Resolve staticAttributeId as distinguished names (true by default) such as in the uniqueMember field of
* groupOfUniqueNames. Set to false to resolve as simple id (as in memberUID of posixGroup for instance).
*/
@XNode("@staticAttributeIdIsDn")
private boolean staticAttributeIdIsDn = true;
@XNode("@staticAttributeId")
protected String staticAttributeId;
@XNode("@dynamicAttributeId")
protected String dynamicAttributeId;
@XNode("@field")
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
public static final List<String> EMPTY_STRING_LIST = Collections.emptyList();
private LDAPFilterMatcher getFilterMatcher() {
return new LDAPFilterMatcher();
}
/**
* @return true if the reference should resolve statically refereed entries (identified by dn-valued attribute)
* @throws DirectoryException
*/
public boolean isStatic() throws DirectoryException {
return getStaticAttributeId() != null;
}
public String getStaticAttributeId() throws DirectoryException {
return getStaticAttributeId(null);
}
public String getStaticAttributeId(DirectoryFieldMapper sourceFM) throws DirectoryException {
if (staticAttributeId != null) {
// explicitly provided attributeId
return staticAttributeId;
}
// sourceFM can be passed to avoid infinite loop in LDAPDirectory init
if (sourceFM == null) {
sourceFM = ((LDAPDirectory) getSourceDirectory()).getFieldMapper();
}
String backendFieldId = sourceFM.getBackendField(fieldName);
if (fieldName.equals(backendFieldId)) {
// no specific backendField found and no staticAttributeId found
// either, this reference should not be statically resolved
return null;
} else {
// BBB: the field mapper has been explicitly used to specify the
// staticAttributeId value as this was the case before the
// introduction of the staticAttributeId dynamicAttributeId duality
log.warn(String.format("implicit static attribute definition through fieldMapping is deprecated, "
+ "please update your setup with "
+ "<ldapReference field=\"%s\" directory=\"%s\" staticAttributeId=\"%s\">", fieldName,
sourceDirectoryName, backendFieldId));
return backendFieldId;
}
}
public List<LDAPDynamicReferenceDescriptor> getDynamicAttributes() {
return Arrays.asList(dynamicReferences);
}
public String getDynamicAttributeId() {
return dynamicAttributeId;
}
/**
* @return true if the reference should resolve dynamically refereed entries (identified by a LDAP url-valued
* attribute)
*/
public boolean isDynamic() {
return dynamicAttributeId != null;
}
@Override
@XNode("@directory")
public void setTargetDirectoryName(String targetDirectoryName) {
this.targetDirectoryName = targetDirectoryName;
}
@Override
public Directory getSourceDirectory() throws DirectoryException {
Directory sourceDir = super.getSourceDirectory();
if (sourceDir instanceof LDAPDirectory) {
return sourceDir;
} else {
throw new DirectoryException(sourceDirectoryName
+ " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName);
}
}
@Override
public Directory getTargetDirectory() throws DirectoryException {
Directory targetDir = super.getTargetDirectory();
if (targetDir instanceof LDAPDirectory) {
return targetDir;
} else {
throw new DirectoryException(targetDirectoryName
+ " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName);
}
}
protected LDAPDirectory getTargetLDAPDirectory() throws DirectoryException {
return (LDAPDirectory) getTargetDirectory();
}
protected LDAPDirectory getSourceLDAPDirectory() throws DirectoryException {
return (LDAPDirectory) getSourceDirectory();
}
protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() throws DirectoryException {
if (targetDirectoryDescriptor == null) {
targetDirectoryDescriptor = getTargetLDAPDirectory().getDescriptor();
}
return targetDirectoryDescriptor;
}
/**
* Store new links using the LDAP staticAttributeId strategy.
*
* @see org.nuxeo.ecm.directory.Reference#addLinks(String, List)
*/
@Override
public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException {
if (targetIds.isEmpty()) {
// optim: nothing to do, return silently without further creating
// session instances
return;
}
LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
String attributeId = getStaticAttributeId();
if (attributeId == null) {
if (log.isTraceEnabled()) {
log.trace(String.format("trying to edit a non-static reference from %s in directory %s: ignoring",
sourceId, ldapSourceDirectory.getName()));
}
return;
}
try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
// fetch the entry to be able to run the security policy
// implemented in an entry adaptor
DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
if (sourceEntry == null) {
throw new DirectoryException(String.format("could not add links from unexisting %s in directory %s",
sourceId, ldapSourceDirectory.getName()));
}
if (!BaseSession.isReadOnlyEntry(sourceEntry)) {
SearchResult ldapEntry = sourceSession.getLdapEntry(sourceId);
String sourceDn = ldapEntry.getNameInNamespace();
Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
Attribute attrToAdd = new BasicAttribute(attributeId);
for (String targetId : targetIds) {
if (staticAttributeIdIsDn) {
// TODO optim: avoid LDAP search request when targetDn
// can be forged client side (rdnAttribute = idAttribute and scope is onelevel)
ldapEntry = targetSession.getLdapEntry(targetId);
if (ldapEntry == null) {
log.warn(String.format(
"entry '%s' in directory '%s' not found: could not add link from '%s' in directory '%s' for '%s'",
targetId, ldapTargetDirectory.getName(), sourceId, ldapSourceDirectory.getName(),
this));
continue;
}
String dn = ldapEntry.getNameInNamespace();
if (storedAttr == null || !storedAttr.contains(dn)) {
attrToAdd.add(dn);
}
} else {
if (storedAttr == null || !storedAttr.contains(targetId)) {
attrToAdd.add(targetId);
}
}
}
if (attrToAdd.size() > 0) {
try {
// do the LDAP request to store missing dns
Attributes attrsToAdd = new BasicAttributes();
attrsToAdd.put(attrToAdd);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s' "
+ "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]", sourceId,
StringUtils.join(targetIds, ", "), sourceDn, attrsToAdd, this));
}
sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrsToAdd);
// robustly clean any existing empty marker now that we are sure that the list in not empty
if (storedAttr.contains(emptyRefMarker)) {
Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);
if (log.isDebugEnabled()) {
log.debug(String.format(
"LDAPReference.addLinks(%s, [%s]): LDAP modifyAttributes dn='%s'"
+ " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]", sourceId,
StringUtils.join(targetIds, ", "), sourceDn, cleanAttrs, this));
}
sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, cleanAttrs);
}
} catch (SchemaViolationException e) {
if (isDynamic()) {
// we are editing an entry that has no static part
log.warn(String.format("cannot update dynamic reference in field %s for source %s",
getFieldName(), sourceId));
} else {
// this is a real schema configuration problem,
// wrap up the exception
throw new DirectoryException(e);
}
}
}
}
} catch (NamingException e) {
throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
}
}
/**
* Store new links using the LDAP staticAttributeId strategy.
*
* @see org.nuxeo.ecm.directory.Reference#addLinks(List, String)
*/
@Override
public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException {
String attributeId = getStaticAttributeId();
if (attributeId == null && !sourceIds.isEmpty()) {
log.warn("trying to edit a non-static reference: ignoring");
return;
}
LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
if (!sourceSession.isReadOnly()) {
// compute the target dn to add to all the matching source
// entries
SearchResult ldapEntry = targetSession.getLdapEntry(targetId);
if (ldapEntry == null) {
throw new DirectoryException(String.format("could not add links to unexisting %s in directory %s",
targetId, ldapTargetDirectory.getName()));
}
String targetAttributeValue;
if (staticAttributeIdIsDn) {
targetAttributeValue = ldapEntry.getNameInNamespace();
} else {
targetAttributeValue = targetId;
}
for (String sourceId : sourceIds) {
// fetch the entry to be able to run the security policy
// implemented in an entry adaptor
DocumentModel sourceEntry = sourceSession.getEntry(sourceId, false);
if (sourceEntry == null) {
log.warn(String.format(
"entry %s in directory %s not found: could not add link to %s in directory %s",
sourceId, ldapSourceDirectory.getName(), targetId, ldapTargetDirectory.getName()));
continue;
}
if (BaseSession.isReadOnlyEntry(sourceEntry)) {
// skip this entry since it cannot be edited to add the
// reference to targetId
log.warn(String.format(
"entry %s in directory %s is readonly: could not add link to %s in directory %s",
sourceId, ldapSourceDirectory.getName(), targetId, ldapTargetDirectory.getName()));
continue;
}
ldapEntry = sourceSession.getLdapEntry(sourceId);
String sourceDn = ldapEntry.getNameInNamespace();
Attribute storedAttr = ldapEntry.getAttributes().get(attributeId);
if (storedAttr.contains(targetAttributeValue)) {
// no need to readd
continue;
}
try {
// add the new dn
Attributes attrs = new BasicAttributes(attributeId, targetAttributeValue);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.addLinks([%s], %s): LDAP modifyAttributes dn='%s'"
+ " mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]", StringUtils.join(sourceIds, ", "),
targetId, sourceDn, attrs, this));
}
sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.ADD_ATTRIBUTE, attrs);
// robustly clean any existing empty marker now that we
// are sure that the list in not empty
if (storedAttr.contains(emptyRefMarker)) {
Attributes cleanAttrs = new BasicAttributes(attributeId, emptyRefMarker);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.addLinks(%s, %s): LDAP modifyAttributes dn='%s'"
+ " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]",
StringUtils.join(sourceIds, ", "), targetId, sourceDn, cleanAttrs.toString(),
this));
}
sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, cleanAttrs);
}
} catch (SchemaViolationException e) {
if (isDynamic()) {
// we are editing an entry that has no static part
log.warn(String.format("cannot add dynamic reference in field %s for target %s",
getFieldName(), targetId));
} else {
// this is a real schema configuration problem,
// wrap the exception
throw new DirectoryException(e);
}
}
}
}
} catch (NamingException e) {
throw new DirectoryException("addLinks failed: " + e.getMessage(), e);
}
}
/**
* Fetch both statically and dynamically defined references and merge the results.
*
* @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
*/
@Override
public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException {
// container to hold merged references
Set<String> sourceIds = new TreeSet<>();
SearchResult targetLdapEntry = null;
String targetDn = null;
// step #1: resolve static references
String staticAttributeId = getStaticAttributeId();
if (staticAttributeId != null) {
// step #1.1: fetch the dn of the targetId entry in the target
// directory by the static dn valued strategy
LDAPDirectory targetDir = getTargetLDAPDirectory();
if (staticAttributeIdIsDn) {
try (LDAPSession targetSession = (LDAPSession) targetDir.getSession()) {
targetLdapEntry = targetSession.getLdapEntry(targetId, false);
if (targetLdapEntry == null) {
String msg = String.format("Failed to perform inverse lookup on LDAPReference"
+ " resolving field '%s' of '%s' to entries of '%s'"
+ " using the static content of attribute '%s':"
+ " entry '%s' cannot be found in '%s'", fieldName, sourceDirectory,
targetDirectoryName, staticAttributeId, targetId, targetDirectoryName);
throw new DirectoryEntryNotFoundException(msg);
}
targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
} catch (NamingException e) {
throw new DirectoryException("error fetching " + targetId + " from " + targetDirectoryName + ": "
+ e.getMessage(), e);
}
}
// step #1.2: search for entries that reference that dn in the
// source directory and collect their ids
LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory();
String filterExpr = String.format("(&(%s={0})%s)", staticAttributeId, ldapSourceDirectory.getBaseFilter());
String[] filterArgs = new String[1];
if (staticAttributeIdIsDn) {
filterArgs[0] = targetDn;
} else {
filterArgs[0] = targetId;
}
String searchBaseDn = ldapSourceDirectory.getDescriptor().getSearchBaseDn();
SearchControls sctls = ldapSourceDirectory.getSearchControls();
try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
+ " filter='%s' args='%s' scope='%s' [%s]", targetId, searchBaseDn, filterExpr,
StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), this));
}
NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(searchBaseDn, filterExpr,
filterArgs, sctls);
try {
while (results.hasMore()) {
Attributes attributes = results.next().getAttributes();
// NXP-2461: check that id field is filled
Attribute attr = attributes.get(sourceSession.idAttribute);
if (attr != null) {
Object value = attr.get();
if (value != null) {
sourceIds.add(value.toString());
}
}
}
} finally {
results.close();
}
} catch (NamingException e) {
throw new DirectoryException("error during reference search for " + filterArgs[0], e);
}
}
// step #2: resolve dynamic references
String dynamicAttributeId = this.dynamicAttributeId;
if (dynamicAttributeId != null) {
LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory();
LDAPDirectory ldapTargetDirectory = getTargetLDAPDirectory();
String searchBaseDn = ldapSourceDirectory.getDescriptor().getSearchBaseDn();
try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession();
LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
// step #2.1: fetch the target entry to apply the ldap url
// filters of the candidate sources on it
if (targetLdapEntry == null) {
// only fetch the entry if not already fetched by the
// static
// attributes references resolution
targetLdapEntry = targetSession.getLdapEntry(targetId, false);
}
if (targetLdapEntry == null) {
String msg = String.format("Failed to perform inverse lookup on LDAPReference"
+ " resolving field '%s' of '%s' to entries of '%s'"
+ " using the dynamic content of attribute '%s':" + " entry '%s' cannot be found in '%s'",
fieldName, ldapSourceDirectory, targetDirectoryName, dynamicAttributeId, targetId,
targetDirectoryName);
throw new DirectoryException(msg);
}
targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
Attributes targetAttributes = targetLdapEntry.getAttributes();
// step #2.2: find the list of entries that hold candidate
// dynamic links in the source directory
SearchControls sctls = ldapSourceDirectory.getSearchControls();
sctls.setReturningAttributes(new String[] { sourceSession.idAttribute, dynamicAttributeId });
String filterExpr = String.format("%s=*", dynamicAttributeId);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'"
+ " filter='%s' scope='%s' [%s]", targetId, searchBaseDn, filterExpr,
sctls.getSearchScope(), this));
}
NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(searchBaseDn, filterExpr,
sctls);
try {
while (results.hasMore()) {
// step #2.3: for each sourceId and each ldapUrl test
// whether the current target entry matches the
// collected
// URL
Attributes sourceAttributes = results.next().getAttributes();
NamingEnumeration<?> ldapUrls = sourceAttributes.get(dynamicAttributeId).getAll();
try {
while (ldapUrls.hasMore()) {
LdapURL ldapUrl = new LdapURL(ldapUrls.next().toString());
String candidateDN = pseudoNormalizeDn(ldapUrl.getDN());
// check base URL
if (!targetDn.endsWith(candidateDN)) {
continue;
}
// check onelevel scope constraints
if ("onelevel".equals(ldapUrl.getScope())) {
int targetDnSize = new LdapName(targetDn).size();
int urlDnSize = new LdapName(candidateDN).size();
if (targetDnSize - urlDnSize > 1) {
// target is not a direct child of the
// DN of the
// LDAP URL
continue;
}
}
// check that the target entry matches the
// filter
if (getFilterMatcher().match(targetAttributes, ldapUrl.getFilter())) {
// the target match the source url, add it
// to the
// collected ids
sourceIds.add(sourceAttributes.get(sourceSession.idAttribute).get().toString());
}
}
} finally {
ldapUrls.close();
}
}
} finally {
results.close();
}
} catch (NamingException e) {
throw new DirectoryException("error during reference search for " + targetId, e);
}
}
/*
* This kind of reference is not supported because Active Directory use filter expression not yet supported by
* LDAPFilterMatcher. See NXP-4562
*/
if (dynamicReferences != null && dynamicReferences.length > 0) {
log.error("This kind of reference is not supported.");
}
return new ArrayList<>(sourceIds);
}
/**
* Fetches both statically and dynamically defined references and merges the results.
*
* @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String)
*/
@Override
// XXX: broken, use getLdapTargetIds for a proper implementation
@SuppressWarnings("unchecked")
public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException {
String schemaName = getSourceDirectory().getSchema();
try (Session session = getSourceDirectory().getSession()) {
try {
return (List<String>) session.getEntry(sourceId).getProperty(schemaName, fieldName);
} catch (PropertyException e) {
throw new DirectoryException(e);
}
}
}
/**
* Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for
* comparison purpose.
*
* @param dn the raw unnormalized dn
* @return lowercase version without whitespace after commas
* @throws InvalidNameException
*/
protected static String pseudoNormalizeDn(String dn) throws InvalidNameException {
LdapName ldapName = new LdapName(dn);
List<String> rdns = new ArrayList<>();
for (Rdn rdn : ldapName.getRdns()) {
String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,");
String rdnStr = rdn.getType().toLowerCase() + "=" + value;
rdns.add(0, rdnStr);
}
return StringUtils.join(rdns, ',');
}
/**
* Optimized method to spare a LDAP request when the caller is a LDAPSession object that has already fetched the
* LDAP Attribute instances.
* <p>
* This method should return the same results as the sister method: org.nuxeo
* .ecm.directory.Reference#getTargetIdsForSource(java.lang.String)
*
* @return target reference ids
* @throws DirectoryException
*/
public List<String> getLdapTargetIds(Attributes attributes) throws DirectoryException {
Set<String> targetIds = new TreeSet<>();
LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
String emptyRefMarker = ldapTargetDirectory.getDescriptor().getEmptyRefMarker();
try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
String baseDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
// step #1: fetch ids referenced by static attributes
String staticAttributeId = getStaticAttributeId();
Attribute staticAttribute = null;
if (staticAttributeId != null) {
staticAttribute = attributes.get(staticAttributeId);
}
if (staticAttribute != null && !staticAttributeIdIsDn) {
NamingEnumeration<?> staticContent = staticAttribute.getAll();
try {
while (staticContent.hasMore()) {
String value = staticContent.next().toString();
if (!emptyRefMarker.equals(value)) {
targetIds.add(value);
}
}
} finally {
staticContent.close();
}
}
if (staticAttribute != null && staticAttributeIdIsDn) {
NamingEnumeration<?> targetDns = staticAttribute.getAll();
try {
while (targetDns.hasMore()) {
String targetDn = targetDns.next().toString();
if (!pseudoNormalizeDn(targetDn).endsWith(baseDn)) {
// optim: avoid network connections when obvious
if (log.isTraceEnabled()) {
log.trace(String.format("ignoring: dn='%s' (does not match '%s') for '%s'", targetDn,
baseDn, this));
}
continue;
}
// find the id of the referenced entry
String id = null;
if (targetSession.rdnMatchesIdField()) {
// optim: do not fetch the entry to get its true id
// but
// guess it by reading the targetDn
LdapName name = new LdapName(targetDn);
String rdn = name.get(name.size() - 1);
int pos = rdn.indexOf("=");
id = rdn.substring(pos + 1);
} else {
id = getIdForDn(targetSession, targetDn);
if (id == null) {
log.warn(String.format(
"ignoring target '%s' (missing attribute '%s') while resolving reference '%s'",
targetDn, targetSession.idAttribute, this));
continue;
}
}
if (forceDnConsistencyCheck) {
// check that the referenced entry is actually part
// of
// the target directory (takes care of the filters
// and
// the scope)
// this check can be very expensive on large groups
// and thus not enabled by default
if (!targetSession.hasEntry(id)) {
if (log.isTraceEnabled()) {
log.trace(String.format(
"ignoring target '%s' when resolving '%s' (not part of target"
+ " directory by forced DN consistency check)", targetDn, this));
}
continue;
}
}
// NXP-2461: check that id field is filled
if (id != null) {
targetIds.add(id);
}
}
} finally {
targetDns.close();
}
}
// step #2: fetched dynamically referenced ids
String dynamicAttributeId = this.dynamicAttributeId;
Attribute dynamicAttribute = null;
if (dynamicAttributeId != null) {
dynamicAttribute = attributes.get(dynamicAttributeId);
}
if (dynamicAttribute != null) {
NamingEnumeration<?> rawldapUrls = dynamicAttribute.getAll();
try {
while (rawldapUrls.hasMore()) {
LdapURL ldapUrl = new LdapURL(rawldapUrls.next().toString());
String linkDn = pseudoNormalizeDn(ldapUrl.getDN());
String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
int scope = SearchControls.ONELEVEL_SCOPE;
String scopePart = ldapUrl.getScope();
if (scopePart != null && scopePart.toLowerCase().startsWith("sub")) {
scope = SearchControls.SUBTREE_SCOPE;
}
if (!linkDn.endsWith(directoryDn) && !directoryDn.endsWith(linkDn)) {
// optim #1: if the dns do not match, abort
continue;
} else if (directoryDn.endsWith(linkDn) && linkDn.length() < directoryDn.length()
&& scope == SearchControls.ONELEVEL_SCOPE) {
// optim #2: the link dn is pointing to elements
// that at
// upperlevel than directory elements
continue;
} else {
// Search for references elements
targetIds.addAll(getReferencedElements(attributes, directoryDn, linkDn,
ldapUrl.getFilter(), scope));
}
}
} finally {
rawldapUrls.close();
}
}
if (dynamicReferences != null && dynamicReferences.length > 0) {
// Only the first Dynamic Reference is used
LDAPDynamicReferenceDescriptor dynAtt = dynamicReferences[0];
Attribute baseDnsAttribute = attributes.get(dynAtt.baseDN);
Attribute filterAttribute = attributes.get(dynAtt.filter);
if (baseDnsAttribute != null && filterAttribute != null) {
NamingEnumeration<?> baseDns = null;
NamingEnumeration<?> filters = null;
try {
// Get the BaseDN value from the descriptor
baseDns = baseDnsAttribute.getAll();
String linkDnValue = baseDns.next().toString();
baseDns.close();
linkDnValue = pseudoNormalizeDn(linkDnValue);
// Get the filter value from the descriptor
filters = filterAttribute.getAll();
String filterValue = filters.next().toString();
filters.close();
// Get the scope value from the descriptor
int scope = "subtree".equalsIgnoreCase(dynAtt.type) ? SearchControls.SUBTREE_SCOPE
: SearchControls.ONELEVEL_SCOPE;
String directoryDn = pseudoNormalizeDn(targetDirconfig.getSearchBaseDn());
// if the dns match, and if the link dn is pointing to
// elements that at upperlevel than directory elements
if ((linkDnValue.endsWith(directoryDn) || directoryDn.endsWith(linkDnValue))
&& !(directoryDn.endsWith(linkDnValue) && linkDnValue.length() < directoryDn.length() && scope == SearchControls.ONELEVEL_SCOPE)) {
// Correct the filter expression
filterValue = FilterExpressionCorrector.correctFilter(filterValue, FilterJobs.CORRECT_NOT);
// Search for references elements
targetIds.addAll(getReferencedElements(attributes, directoryDn, linkDnValue, filterValue,
scope));
}
} finally {
if (baseDns != null) {
baseDns.close();
}
if (filters != null) {
filters.close();
}
}
}
}
// return merged attributes
return new ArrayList<String>(targetIds);
} catch (NamingException e) {
throw new DirectoryException("error computing LDAP references", e);
}
}
protected String getIdForDn(LDAPSession session, String dn) {
// the entry id is not based on the rdn, we thus need to
// fetch the LDAP entry to grab it
String[] attributeIdsToCollect = { session.idAttribute };
Attributes entry;
try {
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.getIdForDn(session, %s): LDAP get dn='%s'"
+ " attribute ids to collect='%s' [%s]", dn, dn, StringUtils.join(attributeIdsToCollect, ", "),
this));
}
Name name = new CompositeName().add(dn);
entry = session.dirContext.getAttributes(name, attributeIdsToCollect);
} catch (NamingException e) {
return null;
}
// NXP-2461: check that id field is filled
Attribute attr = entry.get(session.idAttribute);
if (attr != null) {
try {
return attr.get().toString();
} catch (NamingException e) {
}
}
return null;
}
/**
* Retrieve the elements referenced by the filter/BaseDN/Scope request.
*
* @param attributes Attributes of the referencer element
* @param directoryDn Dn of the Directory
* @param linkDn Dn specified in the parent
* @param filter Filter expression specified in the parent
* @param scope scope for the search
* @return The list of the referenced elements.
* @throws DirectoryException
* @throws NamingException
*/
private Set<String> getReferencedElements(Attributes attributes, String directoryDn, String linkDn, String filter,
int scope) throws DirectoryException, NamingException {
Set<String> targetIds = new TreeSet<>();
LDAPDirectoryDescriptor targetDirconfig = getTargetDirectoryDescriptor();
LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
// use the most specific scope between the one specified in the
// Directory and the specified in the Parent
String dn = directoryDn.endsWith(linkDn) && directoryDn.length() > linkDn.length() ? directoryDn : linkDn;
// combine the ldapUrl search query with target
// directory own constraints
SearchControls scts = new SearchControls();
// use the most specific scope
scts.setSearchScope(Math.min(scope, targetDirconfig.getSearchScope()));
// only fetch the ids of the targets
scts.setReturningAttributes(new String[] { targetSession.idAttribute });
// combine the filter of the target directory with the
// provided filter if any
String targetFilter = targetDirconfig.getSearchFilter();
if (filter == null || filter.length() == 0) {
filter = targetFilter;
} else if (targetFilter != null && targetFilter.length() > 0) {
filter = String.format("(&(%s)(%s))", targetFilter, filter);
}
// perform the request and collect the ids
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.getLdapTargetIds(%s): LDAP search dn='%s' "
+ " filter='%s' scope='%s' [%s]", attributes, dn, dn, scts.getSearchScope(), this));
}
Name name = new CompositeName().add(dn);
NamingEnumeration<SearchResult> results = targetSession.dirContext.search(name, filter, scts);
try {
while (results.hasMore()) {
// NXP-2461: check that id field is filled
Attribute attr = results.next().getAttributes().get(targetSession.idAttribute);
if (attr != null) {
String collectedId = attr.get().toString();
if (collectedId != null) {
targetIds.add(collectedId);
}
}
}
} finally {
results.close();
}
return targetIds;
}
/**
* Remove existing statically defined links for the given source id (dynamic references remain unaltered)
*
* @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String)
*/
@Override
public void removeLinksForSource(String sourceId) throws DirectoryException {
LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
String attributeId = getStaticAttributeId();
try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession();
LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) {
if (sourceSession.isReadOnly() || attributeId == null) {
// do not try to do anything on a read only server or to a
// purely dynamic reference
return;
}
// get the dn of the entry that matches sourceId
SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId);
if (sourceLdapEntry == null) {
throw new DirectoryException(String.format(
"cannot edit the links hold by missing entry '%s' in directory '%s'", sourceId,
ldapSourceDirectory.getName()));
}
String sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace());
Attribute oldAttr = sourceLdapEntry.getAttributes().get(attributeId);
if (oldAttr == null) {
// consider it as an empty attribute to simplify the following
// code
oldAttr = new BasicAttribute(attributeId);
}
Attribute attrToRemove = new BasicAttribute(attributeId);
NamingEnumeration<?> oldAttrs = oldAttr.getAll();
String targetBaseDn = pseudoNormalizeDn(ldapTargetDirectory.getDescriptor().getSearchBaseDn());
try {
while (oldAttrs.hasMore()) {
String targetKeyAttr = oldAttrs.next().toString();
if (staticAttributeIdIsDn) {
String dn = pseudoNormalizeDn(targetKeyAttr);
if (forceDnConsistencyCheck) {
String id = getIdForDn(targetSession, dn);
if (id != null && targetSession.hasEntry(id)) {
// this is an entry managed by the current
// reference
attrToRemove.add(dn);
}
} else if (dn.endsWith(targetBaseDn)) {
// this is an entry managed by the current
// reference
attrToRemove.add(dn);
}
} else {
attrToRemove.add(targetKeyAttr);
}
}
} finally {
oldAttrs.close();
}
try {
if (attrToRemove.size() == oldAttr.size()) {
// use the empty ref marker to avoid empty attr
String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);
if (log.isDebugEnabled()) {
log.debug(String.format(
"LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes key='%s' "
+ " mod_op='REPLACE_ATTRIBUTE' attrs='%s' [%s]", sourceId, sourceDn,
emptyAttribute, this));
}
sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REPLACE_ATTRIBUTE, emptyAttribute);
} else if (attrToRemove.size() > 0) {
// remove the attribute managed by the current reference
Attributes attrsToRemove = new BasicAttributes();
attrsToRemove.put(attrToRemove);
if (log.isDebugEnabled()) {
log.debug(String.format(
"LDAPReference.removeLinksForSource(%s): LDAP modifyAttributes dn='%s' "
+ " mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]", sourceId, sourceDn,
attrsToRemove, this));
}
sourceSession.dirContext.modifyAttributes(sourceDn, DirContext.REMOVE_ATTRIBUTE, attrsToRemove);
}
} catch (SchemaViolationException e) {
if (isDynamic()) {
// we are editing an entry that has no static part
log.warn(String.format("cannot remove dynamic reference in field %s for source %s", getFieldName(),
sourceId));
} else {
// this is a real schma configuration problem, wrapup the
// exception
throw new DirectoryException(e);
}
}
} catch (NamingException e) {
throw new DirectoryException("removeLinksForSource failed: " + e.getMessage(), e);
}
}
/**
* Remove existing statically defined links for the given target id (dynamic references remain unaltered)
*
* @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String)
*/
@Override
public void removeLinksForTarget(String targetId) throws DirectoryException {
if (!isStatic()) {
// nothing to do: dynamic references cannot be updated
return;
}
LDAPDirectory ldapTargetDirectory = (LDAPDirectory) getTargetDirectory();
LDAPDirectory ldapSourceDirectory = (LDAPDirectory) getSourceDirectory();
String attributeId = getStaticAttributeId();
try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession();
LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) {
if (!sourceSession.isReadOnly()) {
// get the dn of the target that matches targetId
String targetAttributeValue;
if (staticAttributeIdIsDn) {
SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId);
if (targetLdapEntry == null) {
String rdnAttribute = ldapTargetDirectory.getDescriptor().getRdnAttribute();
if (!rdnAttribute.equals(targetSession.idAttribute)) {
log.warn(String.format(
"cannot remove links to missing entry %s in directory %s for reference %s",
targetId, ldapTargetDirectory.getName(), this));
return;
}
// the entry might have already been deleted, try to
// re-forge it if possible (might not work if scope is
// subtree)
targetAttributeValue = String.format("%s=%s,%s", rdnAttribute, targetId,
ldapTargetDirectory.getDescriptor().getSearchBaseDn());
} else {
targetAttributeValue = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace());
}
} else {
targetAttributeValue = targetId;
}
// build a LDAP query to find entries that point to the target
String searchFilter = String.format("(%s=%s)", attributeId, targetAttributeValue);
String sourceFilter = ldapSourceDirectory.getBaseFilter();
if (sourceFilter != null && !"".equals(sourceFilter)) {
searchFilter = String.format("(&(%s)(%s))", searchFilter, sourceFilter);
}
SearchControls scts = new SearchControls();
scts.setSearchScope(ldapSourceDirectory.getDescriptor().getSearchScope());
scts.setReturningAttributes(new String[] { attributeId });
// find all source entries that point to the target key and
// clean
// those references
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPReference.removeLinksForTarget(%s): LDAP search baseDn='%s' "
+ " filter='%s' scope='%s' [%s]", targetId, sourceSession.searchBaseDn, searchFilter,
scts.getSearchScope(), this));
}
NamingEnumeration<SearchResult> results = sourceSession.dirContext.search(sourceSession.searchBaseDn,
searchFilter, scts);
String emptyRefMarker = ldapSourceDirectory.getDescriptor().getEmptyRefMarker();
Attributes emptyAttribute = new BasicAttributes(attributeId, emptyRefMarker);
try {
while (results.hasMore()) {
SearchResult result = results.next();
Attributes attrs = result.getAttributes();
Attribute attr = attrs.get(attributeId);
try {
if (attr.size() == 1) {
// the attribute holds the last reference, put
// the
// empty ref. marker before removing the
// attribute
// since empty attribute are often not allowed
// by
// the server schema
if (log.isDebugEnabled()) {
log.debug(String.format(
"LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
+ "mod_op='ADD_ATTRIBUTE' attrs='%s' [%s]", targetId,
result.getNameInNamespace(), attrs, this));
}
sourceSession.dirContext.modifyAttributes(result.getNameInNamespace(),
DirContext.ADD_ATTRIBUTE, emptyAttribute);
}
// remove the reference to the target key
attrs = new BasicAttributes();
attr = new BasicAttribute(attributeId);
attr.add(targetAttributeValue);
attrs.put(attr);
if (log.isDebugEnabled()) {
log.debug(String.format(
"LDAPReference.removeLinksForTarget(%s): LDAP modifyAttributes key='%s' "
+ "mod_op='REMOVE_ATTRIBUTE' attrs='%s' [%s]", targetId,
result.getNameInNamespace(), attrs, this));
}
sourceSession.dirContext.modifyAttributes(result.getNameInNamespace(),
DirContext.REMOVE_ATTRIBUTE, attrs);
} catch (SchemaViolationException e) {
if (isDynamic()) {
// we are editing an entry that has no static
// part
log.warn(String.format("cannot remove dynamic reference in field %s for target %s",
getFieldName(), targetId));
} else {
// this is a real schema configuration problem,
// wrapup the exception
throw new DirectoryException(e);
}
}
}
} finally {
results.close();
}
}
} catch (NamingException e) {
throw new DirectoryException("removeLinksForTarget failed: " + e.getMessage(), e);
}
}
/**
* Edit the list of statically defined references for a given target (dynamic references remain unaltered)
*
* @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List)
*/
@Override
public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException {
removeLinksForTarget(targetId);
addLinks(sourceIds, targetId);
}
/**
* Set the list of statically defined references for a given source (dynamic references remain unaltered)
*
* @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List)
*/
@Override
public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException {
removeLinksForSource(sourceId);
addLinks(sourceId, targetIds);
}
@Override
// to build helpful debug logs
public String toString() {
return String.format("LDAPReference to resolve field='%s' of sourceDirectory='%s'"
+ " with targetDirectory='%s'" + " and staticAttributeId='%s', dynamicAttributeId='%s'", fieldName,
sourceDirectoryName, targetDirectoryName, staticAttributeId, dynamicAttributeId);
}
/**
* @since 5.6
*/
@Override
public LDAPReference clone() {
LDAPReference clone = (LDAPReference) super.clone();
// basic fields are already copied by super.clone()
return clone;
}
}