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