001/* 002 * (C) Copyright 2009-2016 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Anahide Tchertchian 018 */ 019 020package org.nuxeo.ecm.directory.ldap; 021 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.List; 025import java.util.Set; 026import java.util.TreeSet; 027 028import javax.naming.InvalidNameException; 029import javax.naming.NamingEnumeration; 030import javax.naming.NamingException; 031import javax.naming.directory.Attribute; 032import javax.naming.directory.Attributes; 033import javax.naming.directory.SearchControls; 034import javax.naming.directory.SearchResult; 035import javax.naming.ldap.LdapName; 036import javax.naming.ldap.Rdn; 037 038import org.apache.commons.lang.StringUtils; 039import org.apache.commons.logging.Log; 040import org.apache.commons.logging.LogFactory; 041import org.nuxeo.common.xmap.annotation.XNode; 042import org.nuxeo.common.xmap.annotation.XObject; 043import org.nuxeo.ecm.directory.AbstractReference; 044import org.nuxeo.ecm.directory.Directory; 045import org.nuxeo.ecm.directory.DirectoryException; 046import org.nuxeo.ecm.directory.ReferenceDescriptor; 047import org.nuxeo.ecm.directory.Session; 048 049/** 050 * Implementation of the directory Reference interface that makes it possible to retrieve children of a node in the LDAP 051 * tree structure. 052 * 053 * @author Anahide Tchertchian 054 */ 055@XObject(value = "ldapTreeReference") 056public class LDAPTreeReference extends AbstractReference implements Cloneable { 057 058 private static final Log log = LogFactory.getLog(LDAPTreeReference.class); 059 060 public static final List<String> EMPTY_STRING_LIST = Collections.emptyList(); 061 062 protected LDAPDirectoryDescriptor targetDirectoryDescriptor; 063 064 protected int scope; 065 066 public LDAPTreeReference() { 067 super(null, null); 068 } 069 070 public LDAPTreeReference(ReferenceDescriptor referenceDescriptor) { 071 super(referenceDescriptor.getFieldName(), referenceDescriptor.getDirectory()); 072 } 073 074 @XNode("@field") 075 public void setFieldName(String fieldName) { 076 this.fieldName = fieldName; 077 } 078 079 protected LDAPFilterMatcher getFilterMatcher() { 080 return new LDAPFilterMatcher(); 081 } 082 083 @Override 084 @XNode("@directory") 085 public void setTargetDirectoryName(String targetDirectoryName) { 086 this.targetDirectoryName = targetDirectoryName; 087 } 088 089 public int getScope() { 090 return scope; 091 } 092 093 @XNode("@scope") 094 public void setScope(String scope) throws DirectoryException { 095 if (scope == null) { 096 // default value: onelevel 097 this.scope = SearchControls.ONELEVEL_SCOPE; 098 return; 099 } 100 Integer searchScope = LdapScope.getIntegerScope(scope); 101 if (searchScope == null) { 102 // invalid scope 103 throw new DirectoryException( 104 "Invalid search scope: " + scope + ". Valid options: object, onelevel, subtree"); 105 } 106 this.scope = searchScope.intValue(); 107 } 108 109 @Override 110 public Directory getSourceDirectory() throws DirectoryException { 111 112 Directory sourceDir = super.getSourceDirectory(); 113 if (sourceDir instanceof LDAPDirectory) { 114 return sourceDir; 115 } else { 116 throw new DirectoryException(sourceDirectoryName 117 + " is not a LDAPDirectory and thus cannot be used in a reference for " + fieldName); 118 } 119 } 120 121 @Override 122 public Directory getTargetDirectory() throws DirectoryException { 123 Directory targetDir = super.getTargetDirectory(); 124 if (targetDir instanceof LDAPDirectory) { 125 return targetDir; 126 } else { 127 throw new DirectoryException(targetDirectoryName 128 + " is not a LDAPDirectory and thus cannot be referenced as target by " + fieldName); 129 } 130 } 131 132 protected LDAPDirectory getTargetLDAPDirectory() throws DirectoryException { 133 return (LDAPDirectory) getTargetDirectory(); 134 } 135 136 protected LDAPDirectory getSourceLDAPDirectory() throws DirectoryException { 137 return (LDAPDirectory) getSourceDirectory(); 138 } 139 140 protected LDAPDirectoryDescriptor getTargetDirectoryDescriptor() throws DirectoryException { 141 if (targetDirectoryDescriptor == null) { 142 targetDirectoryDescriptor = getTargetLDAPDirectory().getDescriptor(); 143 } 144 return targetDirectoryDescriptor; 145 } 146 147 /** 148 * NOT IMPLEMENTED: Store new links 149 * 150 * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List) 151 */ 152 @Override 153 public void addLinks(String sourceId, List<String> targetIds) throws DirectoryException { 154 // TODO: not yet implemented 155 } 156 157 /** 158 * NOT IMPLEMENTED: Store new links 159 * 160 * @see org.nuxeo.ecm.directory.Reference#addLinks(String, List, Session) 161 */ 162 @Override 163 public void addLinks(String sourceId, List<String> targetIds, Session session) throws DirectoryException { 164 // TODO: not yet implemented 165 } 166 167 @Override 168 public void addLinks(List<String> sourceIds, String targetId, Session session) throws DirectoryException { 169 // TODO: not yet implemented 170 } 171 172 /** 173 * NOT IMPLEMENTED: Store new links. 174 * 175 * @see org.nuxeo.ecm.directory.Reference#addLinks(List, String) 176 */ 177 @Override 178 public void addLinks(List<String> sourceIds, String targetId) throws DirectoryException { 179 // TODO: not yet implemented 180 } 181 182 /** 183 * Fetches single parent, cutting the dn and trying to get the given entry. 184 * 185 * @see org.nuxeo.ecm.directory.Reference#getSourceIdsForTarget(String) 186 */ 187 @Override 188 public List<String> getSourceIdsForTarget(String targetId) throws DirectoryException { 189 Set<String> sourceIds = new TreeSet<>(); 190 String targetDn = null; 191 192 // step #1: fetch the dn of the targetId entry in the target 193 // directory by the static dn valued strategy 194 LDAPDirectory targetDir = getTargetLDAPDirectory(); 195 try (LDAPSession targetSession = (LDAPSession) targetDir.getSession()) { 196 SearchResult targetLdapEntry = targetSession.getLdapEntry(targetId, true); 197 if (targetLdapEntry == null) { 198 // no parent accessible => return empty list 199 return EMPTY_STRING_LIST; 200 } 201 targetDn = pseudoNormalizeDn(targetLdapEntry.getNameInNamespace()); 202 } catch (NamingException e) { 203 throw new DirectoryException("error fetching " + targetId, e); 204 } 205 206 // step #2: search for entries that reference parent dn in the 207 // source directory and collect its id 208 LDAPDirectory ldapSourceDirectory = getSourceLDAPDirectory(); 209 String parentDn = getParentDn(targetDn); 210 String filterExpr = String.format("(&%s)", ldapSourceDirectory.getBaseFilter()); 211 String[] filterArgs = {}; 212 213 // get a copy of original search controls 214 SearchControls sctls = ldapSourceDirectory.getSearchControls(true); 215 sctls.setSearchScope(SearchControls.OBJECT_SCOPE); 216 try (LDAPSession sourceSession = (LDAPSession) ldapSourceDirectory.getSession()) { 217 if (log.isDebugEnabled()) { 218 log.debug(String.format( 219 "LDAPReference.getSourceIdsForTarget(%s): LDAP search search base='%s'" 220 + " filter='%s' args='%s' scope='%s' [%s]", 221 targetId, parentDn, filterExpr, StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), 222 this)); 223 } 224 NamingEnumeration<SearchResult> results = sourceSession.getContext().search(parentDn, filterExpr, 225 filterArgs, sctls); 226 227 try { 228 while (results.hasMore()) { 229 Attributes attributes = results.next().getAttributes(); 230 // NXP-2461: check that id field is filled 231 Attribute attr = attributes.get(sourceSession.idAttribute); 232 if (attr != null) { 233 Object value = attr.get(); 234 if (value != null) { 235 sourceIds.add(value.toString()); 236 // only supposed to get one result anyway 237 break; 238 } 239 } 240 } 241 } finally { 242 results.close(); 243 } 244 } catch (NamingException e) { 245 throw new DirectoryException("error during reference search for " + targetDn, e); 246 } 247 248 return new ArrayList<>(sourceIds); 249 } 250 251 /** 252 * Fetches children, onelevel or subtree given the reference configuration. 253 * <p> 254 * Removes entries with same id than parent to only get real children. 255 * 256 * @see org.nuxeo.ecm.directory.Reference#getTargetIdsForSource(String) 257 */ 258 // TODO: optimize reusing the same ldap session (see LdapReference optim 259 // method) 260 @Override 261 public List<String> getTargetIdsForSource(String sourceId) throws DirectoryException { 262 Set<String> targetIds = new TreeSet<>(); 263 String sourceDn = null; 264 265 // step #1: fetch the dn of the sourceId entry in the source 266 // directory by the static dn valued strategy 267 LDAPDirectory sourceDir = getSourceLDAPDirectory(); 268 try (LDAPSession sourceSession = (LDAPSession) sourceDir.getSession()) { 269 SearchResult sourceLdapEntry = sourceSession.getLdapEntry(sourceId, true); 270 if (sourceLdapEntry == null) { 271 throw new DirectoryException(sourceId + " does not exist in " + sourceDirectoryName); 272 } 273 sourceDn = pseudoNormalizeDn(sourceLdapEntry.getNameInNamespace()); 274 } catch (NamingException e) { 275 throw new DirectoryException("error fetching " + sourceId, e); 276 } 277 278 // step #2: search for entries with sourceDn as base dn and collect 279 // their ids 280 LDAPDirectory ldapTargetDirectory = getTargetLDAPDirectory(); 281 282 String filterExpr = String.format("(&%s)", ldapTargetDirectory.getBaseFilter()); 283 String[] filterArgs = {}; 284 285 // get a copy of original search controls 286 SearchControls sctls = ldapTargetDirectory.getSearchControls(true); 287 sctls.setSearchScope(getScope()); 288 try (LDAPSession targetSession = (LDAPSession) ldapTargetDirectory.getSession()) { 289 if (log.isDebugEnabled()) { 290 log.debug(String.format( 291 "LDAPReference.getTargetIdsForSource(%s): LDAP search search base='%s'" 292 + " filter='%s' args='%s' scope='%s' [%s]", 293 sourceId, sourceDn, filterExpr, StringUtils.join(filterArgs, ", "), sctls.getSearchScope(), 294 this)); 295 } 296 NamingEnumeration<SearchResult> results = targetSession.getContext().search(sourceDn, filterExpr, 297 filterArgs, sctls); 298 299 try { 300 while (results.hasMore()) { 301 Attributes attributes = results.next().getAttributes(); 302 // NXP-2461: check that id field is filled 303 Attribute attr = attributes.get(targetSession.idAttribute); 304 if (attr != null) { 305 Object value = attr.get(); 306 if (value != null) { 307 // always remove self as child 308 String targetId = value.toString(); 309 if (!sourceId.equals(targetId)) { 310 targetIds.add(targetId); 311 } 312 } 313 } 314 } 315 } finally { 316 results.close(); 317 } 318 } catch (NamingException e) { 319 throw new DirectoryException("error during reference search for " + sourceDn, e); 320 } 321 322 return new ArrayList<>(targetIds); 323 } 324 325 /** 326 * Simple helper that replaces ", " by "," in the provided dn and returns the lower case version of the result for 327 * comparison purpose. 328 * 329 * @param dn the raw unnormalized dn 330 * @return lowercase version without whitespace after commas 331 */ 332 protected static String pseudoNormalizeDn(String dn) throws InvalidNameException { 333 LdapName ldapName = new LdapName(dn); 334 List<String> rdns = new ArrayList<>(); 335 for (Rdn rdn : ldapName.getRdns()) { 336 String value = rdn.getValue().toString().toLowerCase().replaceAll(",", "\\\\,"); 337 String rdnStr = rdn.getType().toLowerCase() + "=" + value; 338 rdns.add(0, rdnStr); 339 } 340 return StringUtils.join(rdns, ','); 341 } 342 343 protected String getParentDn(String dn) { 344 LdapName ldapName; 345 String parentDn; 346 347 if (dn != null) { 348 try { 349 ldapName = new LdapName(dn); 350 ldapName.remove(ldapName.size() - 1); 351 parentDn = ldapName.toString(); 352 return parentDn; 353 354 } catch (InvalidNameException ex) { 355 return null; 356 } 357 } 358 return null; 359 } 360 361 /** 362 * NOT IMPLEMENTED: Remove existing statically defined links for the given source id 363 * 364 * @see org.nuxeo.ecm.directory.Reference#removeLinksForSource(String) 365 */ 366 @Override 367 public void removeLinksForSource(String sourceId) throws DirectoryException { 368 // TODO: not yet implemented 369 } 370 371 /** 372 * NOT IMPLEMENTED: Remove existing statically defined links for the given target id 373 * 374 * @see org.nuxeo.ecm.directory.Reference#removeLinksForTarget(String) 375 */ 376 @Override 377 public void removeLinksForTarget(String targetId) throws DirectoryException { 378 // TODO: not yet implemented 379 } 380 381 /** 382 * NOT IMPLEMENTED: Edit the list of statically defined references for a given target 383 * 384 * @see org.nuxeo.ecm.directory.Reference#setSourceIdsForTarget(String, List) 385 */ 386 @Override 387 public void setSourceIdsForTarget(String targetId, List<String> sourceIds) throws DirectoryException { 388 // TODO: not yet implemented 389 } 390 391 @Override 392 public void setSourceIdsForTarget(String targetId, List<String> sourceIds, Session session) 393 throws DirectoryException { 394 // TODO: not yet implemented 395 } 396 397 /** 398 * NOT IMPLEMENTED: Set the list of statically defined references for a given source 399 * 400 * @see org.nuxeo.ecm.directory.Reference#setTargetIdsForSource(String, List) 401 */ 402 @Override 403 public void setTargetIdsForSource(String sourceId, List<String> targetIds) throws DirectoryException { 404 // TODO: not yet implemented 405 } 406 407 @Override 408 public void setTargetIdsForSource(String sourceId, List<String> targetIds, Session session) 409 throws DirectoryException { 410 // TODO: not yet implemented 411 } 412 413 @Override 414 public void removeLinksForTarget(String targetId, Session session) throws DirectoryException { 415 // TODO: not yet implemented 416 } 417 418 @Override 419 public void removeLinksForSource(String sourceId, Session session) throws DirectoryException { 420 // TODO: not yet implemented 421 } 422 423 @Override 424 // to build helpful debug logs 425 public String toString() { 426 return String.format( 427 "LDAPTreeReference to resolve field='%s' of sourceDirectory='%s'" + " with targetDirectory='%s'", 428 fieldName, sourceDirectoryName, targetDirectoryName); 429 } 430 431 /** 432 * @since 5.6 433 */ 434 @Override 435 public LDAPTreeReference clone() { 436 try { 437 // basic fields are already copied by super.clone() 438 return (LDAPTreeReference) super.clone(); 439 } catch (CloneNotSupportedException e) { 440 throw new AssertionError(e); 441 } 442 } 443 444}