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