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