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