001/* 002 * (C) Copyright 2006-2014 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 * Nuxeo - initial API and implementation 018 * 019 */ 020 021package org.nuxeo.ecm.directory.ldap; 022 023import java.io.IOException; 024import java.io.Serializable; 025import java.text.ParseException; 026import java.text.SimpleDateFormat; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Calendar; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.Date; 033import java.util.HashMap; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Map; 037import java.util.Properties; 038import java.util.Set; 039import java.util.SimpleTimeZone; 040 041import javax.naming.Context; 042import javax.naming.LimitExceededException; 043import javax.naming.NameNotFoundException; 044import javax.naming.NamingEnumeration; 045import javax.naming.NamingException; 046import javax.naming.SizeLimitExceededException; 047import javax.naming.directory.Attribute; 048import javax.naming.directory.Attributes; 049import javax.naming.directory.BasicAttribute; 050import javax.naming.directory.BasicAttributes; 051import javax.naming.directory.DirContext; 052import javax.naming.directory.SearchControls; 053import javax.naming.directory.SearchResult; 054import javax.naming.ldap.InitialLdapContext; 055 056import org.apache.commons.lang.StringUtils; 057import org.apache.commons.logging.Log; 058import org.apache.commons.logging.LogFactory; 059import org.nuxeo.ecm.core.api.Blob; 060import org.nuxeo.ecm.core.api.Blobs; 061import org.nuxeo.ecm.core.api.DocumentModel; 062import org.nuxeo.ecm.core.api.DocumentModelList; 063import org.nuxeo.ecm.core.api.PropertyException; 064import org.nuxeo.ecm.core.api.RecoverableClientException; 065import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 066import org.nuxeo.ecm.core.api.security.SecurityConstants; 067import org.nuxeo.ecm.core.schema.types.Field; 068import org.nuxeo.ecm.core.schema.types.Type; 069import org.nuxeo.ecm.core.utils.SIDGenerator; 070import org.nuxeo.ecm.directory.BaseSession; 071import org.nuxeo.ecm.directory.DirectoryException; 072import org.nuxeo.ecm.directory.DirectoryFieldMapper; 073import org.nuxeo.ecm.directory.EntryAdaptor; 074import org.nuxeo.ecm.directory.EntrySource; 075import org.nuxeo.ecm.directory.PasswordHelper; 076import org.nuxeo.ecm.directory.Reference; 077import org.nuxeo.ecm.directory.BaseDirectoryDescriptor.SubstringMatchType; 078 079/** 080 * This class represents a session against an LDAPDirectory. 081 * 082 * @author Olivier Grisel <ogrisel@nuxeo.com> 083 */ 084public class LDAPSession extends BaseSession implements EntrySource { 085 086 protected static final String MISSING_ID_LOWER_CASE = "lower"; 087 088 protected static final String MISSING_ID_UPPER_CASE = "upper"; 089 090 private static final Log log = LogFactory.getLog(LDAPSession.class); 091 092 protected final String schemaName; 093 094 protected final DirContext dirContext; 095 096 protected final String idAttribute; 097 098 protected final String idCase; 099 100 protected final String searchBaseDn; 101 102 protected final Set<String> emptySet = Collections.emptySet(); 103 104 protected final String sid; 105 106 protected final Map<String, Field> schemaFieldMap; 107 108 protected SubstringMatchType substringMatchType; 109 110 protected final String rdnAttribute; 111 112 protected final String rdnField; 113 114 protected final String passwordHashAlgorithm; 115 116 public LDAPSession(LDAPDirectory directory, DirContext dirContext) { 117 super(directory); 118 this.dirContext = LdapRetryHandler.wrap(dirContext, directory.getServer().getRetries()); 119 DirectoryFieldMapper fieldMapper = directory.getFieldMapper(); 120 idAttribute = fieldMapper.getBackendField(getIdField()); 121 LDAPDirectoryDescriptor descriptor = directory.getDescriptor(); 122 idCase = descriptor.getIdCase(); 123 schemaName = directory.getSchema(); 124 schemaFieldMap = directory.getSchemaFieldMap(); 125 sid = String.valueOf(SIDGenerator.next()); 126 searchBaseDn = descriptor.getSearchBaseDn(); 127 substringMatchType = descriptor.getSubstringMatchType(); 128 rdnAttribute = descriptor.getRdnAttribute(); 129 rdnField = directory.getFieldMapper().getDirectoryField(rdnAttribute); 130 passwordHashAlgorithm = descriptor.passwordHashAlgorithm; 131 permissions = descriptor.permissions; 132 } 133 134 @Override 135 public LDAPDirectory getDirectory() { 136 return (LDAPDirectory) directory; 137 } 138 139 public DirContext getContext() { 140 return dirContext; 141 } 142 143 @Override 144 @SuppressWarnings("unchecked") 145 public DocumentModel createEntry(Map<String, Object> fieldMap) { 146 checkPermission(SecurityConstants.WRITE); 147 LDAPDirectoryDescriptor descriptor = getDirectory().getDescriptor(); 148 List<String> referenceFieldList = new LinkedList<String>(); 149 try { 150 String dn = String.format("%s=%s,%s", rdnAttribute, fieldMap.get(rdnField), descriptor.getCreationBaseDn()); 151 Attributes attrs = new BasicAttributes(); 152 Attribute attr; 153 154 List<String> mandatoryAttributes = getMandatoryAttributes(); 155 for (String mandatoryAttribute : mandatoryAttributes) { 156 attr = new BasicAttribute(mandatoryAttribute); 157 attr.add(" "); 158 attrs.put(attr); 159 } 160 161 String[] creationClasses = descriptor.getCreationClasses(); 162 if (creationClasses.length != 0) { 163 attr = new BasicAttribute("objectclass"); 164 for (String creationClasse : creationClasses) { 165 attr.add(creationClasse); 166 } 167 attrs.put(attr); 168 } 169 170 for (String fieldId : fieldMap.keySet()) { 171 String backendFieldId = getDirectory().getFieldMapper().getBackendField(fieldId); 172 if (backendFieldId.equals(getPasswordField())) { 173 attr = new BasicAttribute(backendFieldId); 174 String password = (String) fieldMap.get(fieldId); 175 password = PasswordHelper.hashPassword(password, passwordHashAlgorithm); 176 attr.add(password); 177 attrs.put(attr); 178 } else if (getDirectory().isReference(fieldId)) { 179 List<Reference> references = directory.getReferences(fieldId); 180 if (references.size() > 1) { 181 // not supported 182 } else { 183 Reference reference = references.get(0); 184 if (reference instanceof LDAPReference) { 185 attr = new BasicAttribute(((LDAPReference) reference).getStaticAttributeId()); 186 attr.add(descriptor.getEmptyRefMarker()); 187 attrs.put(attr); 188 } 189 } 190 referenceFieldList.add(fieldId); 191 } else if (LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY.equals(backendFieldId)) { 192 // ignore special DN field 193 log.warn(String.format("field %s is mapped to read only DN field: ignored", fieldId)); 194 } else { 195 Object value = fieldMap.get(fieldId); 196 if ((value != null) && !value.equals("") && !Collections.emptyList().equals(value)) { 197 attrs.put(getAttributeValue(fieldId, value)); 198 } 199 } 200 } 201 202 if (log.isDebugEnabled()) { 203 String idField = getIdField(); 204 log.debug(String.format("LDAPSession.createEntry(%s=%s): LDAP bind dn='%s' attrs='%s' [%s]", idField, 205 fieldMap.get(idField), dn, attrs, this)); 206 } 207 dirContext.bind(dn, null, attrs); 208 209 for (String referenceFieldName : referenceFieldList) { 210 List<Reference> references = directory.getReferences(referenceFieldName); 211 if (references.size() > 1) { 212 // not supported 213 } else { 214 Reference reference = references.get(0); 215 List<String> targetIds = (List<String>) fieldMap.get(referenceFieldName); 216 reference.addLinks((String) fieldMap.get(getIdField()), targetIds); 217 } 218 } 219 String dnFieldName = getDirectory().getFieldMapper().getDirectoryField(LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY); 220 if (getDirectory().getSchemaFieldMap().containsKey(dnFieldName)) { 221 // add the DN special attribute to the fieldmap of the new 222 // entry 223 fieldMap.put(dnFieldName, dn); 224 } 225 getDirectory().invalidateCaches(); 226 return fieldMapToDocumentModel(fieldMap); 227 } catch (NamingException e) { 228 handleException(e, "createEntry failed"); 229 return null; 230 } 231 } 232 233 @Override 234 public DocumentModel getEntry(String id) throws DirectoryException { 235 return getEntry(id, true); 236 } 237 238 @Override 239 public DocumentModel getEntry(String id, boolean fetchReferences) throws DirectoryException { 240 if (!hasPermission(SecurityConstants.READ)) { 241 return null; 242 } 243 return directory.getCache().getEntry(id, this, fetchReferences); 244 } 245 246 @Override 247 public DocumentModel getEntryFromSource(String id, boolean fetchReferences) throws DirectoryException { 248 try { 249 SearchResult result = getLdapEntry(id, true); 250 if (result == null) { 251 return null; 252 } 253 return ldapResultToDocumentModel(result, id, fetchReferences); 254 } catch (NamingException e) { 255 throw new DirectoryException("getEntry failed: " + e.getMessage(), e); 256 } 257 } 258 259 @Override 260 public boolean hasEntry(String id) throws DirectoryException { 261 try { 262 // TODO: check directory cache first 263 return getLdapEntry(id) != null; 264 } catch (NamingException e) { 265 throw new DirectoryException("hasEntry failed: " + e.getMessage(), e); 266 } 267 } 268 269 protected SearchResult getLdapEntry(String id) throws NamingException, DirectoryException { 270 return getLdapEntry(id, false); 271 } 272 273 protected SearchResult getLdapEntry(String id, boolean fetchAllAttributes) throws NamingException { 274 if (StringUtils.isEmpty(id)) { 275 log.warn("The application should not " + "query for entries with an empty id " + "=> return no results"); 276 return null; 277 } 278 String filterExpr; 279 String baseFilter = getDirectory().getBaseFilter(); 280 if (baseFilter.startsWith("(")) { 281 filterExpr = String.format("(&(%s={0})%s)", idAttribute, baseFilter); 282 } else { 283 filterExpr = String.format("(&(%s={0})(%s))", idAttribute, baseFilter); 284 } 285 String[] filterArgs = { id }; 286 SearchControls scts = getDirectory().getSearchControls(fetchAllAttributes); 287 288 if (log.isDebugEnabled()) { 289 log.debug(String.format("LDAPSession.getLdapEntry(%s, %s): LDAP search base='%s' filter='%s' " 290 + " args='%s' scope='%s' [%s]", id, fetchAllAttributes, searchBaseDn, filterExpr, id, 291 scts.getSearchScope(), this)); 292 } 293 NamingEnumeration<SearchResult> results; 294 try { 295 results = dirContext.search(searchBaseDn, filterExpr, filterArgs, scts); 296 } catch (NameNotFoundException nnfe) { 297 // sometimes ActiveDirectory have some query fail with: LDAP: 298 // error code 32 - 0000208D: NameErr: DSID-031522C9, problem 299 // 2001 (NO_OBJECT). 300 // To keep the application usable return no results instead of 301 // crashing but log the error so that the AD admin 302 // can fix the issue. 303 log.error("Unexpected response from server while performing query: " + nnfe.getMessage(), nnfe); 304 return null; 305 } 306 307 if (!results.hasMore()) { 308 log.debug("Entry not found: " + id); 309 return null; 310 } 311 SearchResult result = results.next(); 312 try { 313 String dn = result.getNameInNamespace(); 314 if (results.hasMore()) { 315 result = results.next(); 316 String dn2 = result.getNameInNamespace(); 317 String msg = String.format("Unable to fetch entry for '%s': found more than one match," 318 + " for instance: '%s' and '%s'", id, dn, dn2); 319 log.error(msg); 320 // ignore entries that are ambiguous while giving enough info 321 // in the logs to let the LDAP admin be able to fix the issue 322 return null; 323 } 324 if (log.isDebugEnabled()) { 325 log.debug(String.format("LDAPSession.getLdapEntry(%s, %s): LDAP search base='%s' filter='%s' " 326 + " args='%s' scope='%s' => found: %s [%s]", id, fetchAllAttributes, searchBaseDn, filterExpr, 327 id, scts.getSearchScope(), dn, this)); 328 } 329 } catch (UnsupportedOperationException e) { 330 // ignore unsupported operation thrown by the Apache DS server in 331 // the tests in embedded mode 332 } 333 return result; 334 } 335 336 @Override 337 public DocumentModelList getEntries() throws DirectoryException { 338 if (!hasPermission(SecurityConstants.READ)) { 339 return new DocumentModelListImpl(); 340 } 341 try { 342 SearchControls scts = getDirectory().getSearchControls(true); 343 if (log.isDebugEnabled()) { 344 log.debug(String.format("LDAPSession.getEntries(): LDAP search base='%s' filter='%s' " 345 + " args=* scope=%s [%s]", searchBaseDn, getDirectory().getBaseFilter(), scts.getSearchScope(), this)); 346 } 347 NamingEnumeration<SearchResult> results = dirContext.search(searchBaseDn, getDirectory().getBaseFilter(), scts); 348 // skip reference fetching 349 return ldapResultsToDocumentModels(results, false); 350 } catch (SizeLimitExceededException e) { 351 throw new org.nuxeo.ecm.directory.SizeLimitExceededException(e); 352 } catch (NamingException e) { 353 throw new DirectoryException("getEntries failed", e); 354 } 355 } 356 357 @Override 358 @SuppressWarnings("unchecked") 359 public void updateEntry(DocumentModel docModel) { 360 checkPermission(SecurityConstants.WRITE); 361 List<String> updateList = new ArrayList<String>(); 362 List<String> referenceFieldList = new LinkedList<String>(); 363 364 try { 365 for (String fieldName : schemaFieldMap.keySet()) { 366 if (!docModel.getPropertyObject(schemaName, fieldName).isDirty()) { 367 continue; 368 } 369 if (getDirectory().isReference(fieldName)) { 370 referenceFieldList.add(fieldName); 371 } else { 372 updateList.add(fieldName); 373 } 374 } 375 376 if (!isReadOnlyEntry(docModel) && !updateList.isEmpty()) { 377 Attributes attrs = new BasicAttributes(); 378 SearchResult ldapEntry = getLdapEntry(docModel.getId()); 379 if (ldapEntry == null) { 380 throw new DirectoryException(docModel.getId() + " not found"); 381 } 382 Attributes oldattrs = ldapEntry.getAttributes(); 383 String dn = ldapEntry.getNameInNamespace(); 384 Attributes attrsToDel = new BasicAttributes(); 385 for (String f : updateList) { 386 Object value = docModel.getProperty(schemaName, f); 387 String backendField = getDirectory().getFieldMapper().getBackendField(f); 388 if (LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY.equals(backendField)) { 389 // skip special LDAP DN field that is readonly 390 log.warn(String.format("field %s is mapped to read only DN field: ignored", f)); 391 continue; 392 } 393 if (value == null || value.equals("")) { 394 Attribute objectClasses = oldattrs.get("objectClass"); 395 Attribute attr; 396 if (getMandatoryAttributes(objectClasses).contains(backendField)) { 397 attr = new BasicAttribute(backendField); 398 // XXX: this might fail if the mandatory attribute 399 // is typed integer for instance 400 attr.add(" "); 401 attrs.put(attr); 402 } else if (oldattrs.get(backendField) != null) { 403 attr = new BasicAttribute(backendField); 404 attr.add(oldattrs.get(backendField).get()); 405 attrsToDel.put(attr); 406 } 407 } else if (f.equals(getPasswordField())) { 408 // The password has been updated, it has to be encrypted 409 Attribute attr = new BasicAttribute(backendField); 410 attr.add(PasswordHelper.hashPassword((String) value, passwordHashAlgorithm)); 411 attrs.put(attr); 412 } else { 413 attrs.put(getAttributeValue(f, value)); 414 } 415 } 416 417 if (log.isDebugEnabled()) { 418 log.debug(String.format("LDAPSession.updateEntry(%s): LDAP modifyAttributes dn='%s' " 419 + "mod_op='REMOVE_ATTRIBUTE' attr='%s' [%s]", docModel, dn, attrsToDel, this)); 420 } 421 dirContext.modifyAttributes(dn, DirContext.REMOVE_ATTRIBUTE, attrsToDel); 422 423 if (log.isDebugEnabled()) { 424 log.debug(String.format("LDAPSession.updateEntry(%s): LDAP modifyAttributes dn='%s' " 425 + "mod_op='REPLACE_ATTRIBUTE' attr='%s' [%s]", docModel, dn, attrs, this)); 426 } 427 dirContext.modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, attrs); 428 } 429 430 // update reference fields 431 for (String referenceFieldName : referenceFieldList) { 432 List<Reference> references = directory.getReferences(referenceFieldName); 433 if (references.size() > 1) { 434 // not supported 435 } else { 436 Reference reference = references.get(0); 437 List<String> targetIds = (List<String>) docModel.getProperty(schemaName, referenceFieldName); 438 reference.setTargetIdsForSource(docModel.getId(), targetIds); 439 } 440 } 441 } catch (NamingException e) { 442 handleException(e, "updateEntry failed:"); 443 } 444 getDirectory().invalidateCaches(); 445 } 446 447 protected void handleException(Exception e, String message) { 448 LdapExceptionProcessor processor = getDirectory().getDescriptor().getExceptionProcessor(); 449 450 RecoverableClientException userException = processor.extractRecoverableException(e); 451 if (userException != null) { 452 throw userException; 453 } 454 throw new DirectoryException(message + " " + e.getMessage(), e); 455 456 } 457 458 @Override 459 public void deleteEntry(DocumentModel dm) { 460 deleteEntry(dm.getId()); 461 } 462 463 @Override 464 public void deleteEntry(String id) { 465 checkPermission(SecurityConstants.WRITE); 466 checkDeleteConstraints(id); 467 try { 468 for (String fieldName : schemaFieldMap.keySet()) { 469 if (getDirectory().isReference(fieldName)) { 470 List<Reference> references = directory.getReferences(fieldName); 471 if (references.size() > 1) { 472 // not supported 473 } else { 474 Reference reference = references.get(0); 475 reference.removeLinksForSource(id); 476 } 477 } 478 } 479 SearchResult result = getLdapEntry(id); 480 481 if (log.isDebugEnabled()) { 482 log.debug(String.format("LDAPSession.deleteEntry(%s): LDAP destroySubcontext dn='%s' [%s]", id, 483 result.getNameInNamespace(), this)); 484 } 485 dirContext.destroySubcontext(result.getNameInNamespace()); 486 } catch (NamingException e) { 487 handleException(e, "deleteEntry failed for: " + id); 488 } 489 getDirectory().invalidateCaches(); 490 } 491 492 @Override 493 public void deleteEntry(String id, Map<String, String> map) { 494 log.warn("Calling deleteEntry extended on LDAP directory"); 495 deleteEntry(id); 496 } 497 498 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, boolean fetchReferences, 499 Map<String, String> orderBy) throws DirectoryException { 500 if (!hasPermission(SecurityConstants.READ)) { 501 return new DocumentModelListImpl(); 502 } 503 try { 504 // building the query using filterExpr / filterArgs to 505 // escape special characters and to fulltext search only on 506 // the explicitly specified fields 507 String[] filters = new String[filter.size()]; 508 String[] filterArgs = new String[filter.size()]; 509 510 if (fulltext == null) { 511 fulltext = Collections.emptySet(); 512 } 513 514 int index = 0; 515 for (String fieldName : filter.keySet()) { 516 if (getDirectory().isReference(fieldName)) { 517 log.warn(fieldName + " is a reference and will be ignored as a query criterion"); 518 continue; 519 } 520 521 String backendFieldName = getDirectory().getFieldMapper().getBackendField(fieldName); 522 Object fieldValue = filter.get(fieldName); 523 524 StringBuilder currentFilter = new StringBuilder(); 525 currentFilter.append("("); 526 if (fieldValue == null) { 527 currentFilter.append("!(" + backendFieldName + "=*)"); 528 } else if ("".equals(fieldValue)) { 529 if (fulltext.contains(fieldName)) { 530 currentFilter.append(backendFieldName + "=*"); 531 } else { 532 currentFilter.append("!(" + backendFieldName + "=*)"); 533 } 534 } else { 535 currentFilter.append(backendFieldName + "="); 536 if (fulltext.contains(fieldName)) { 537 switch (substringMatchType) { 538 case subinitial: 539 currentFilter.append("{" + index + "}*"); 540 break; 541 case subfinal: 542 currentFilter.append("*{" + index + "}"); 543 break; 544 case subany: 545 currentFilter.append("*{" + index + "}*"); 546 break; 547 } 548 } else { 549 currentFilter.append("{" + index + "}"); 550 } 551 } 552 currentFilter.append(")"); 553 filters[index] = currentFilter.toString(); 554 if (fieldValue != null && !"".equals(fieldValue)) { 555 if (fieldValue instanceof Blob) { 556 // filter arg could be a sequence of \xx where xx is the 557 // hexadecimal value of the byte 558 log.warn("Binary search is not supported"); 559 } else { 560 // XXX: what kind of Objects can we get here? Is 561 // toString() enough? 562 filterArgs[index] = fieldValue.toString(); 563 } 564 } 565 index++; 566 } 567 String filterExpr = "(&" + getDirectory().getBaseFilter() + StringUtils.join(filters) + ')'; 568 SearchControls scts = getDirectory().getSearchControls(true); 569 570 if (log.isDebugEnabled()) { 571 log.debug(String.format( 572 "LDAPSession.query(...): LDAP search base='%s' filter='%s' args='%s' scope='%s' [%s]", 573 searchBaseDn, filterExpr, StringUtils.join(filterArgs, ","), scts.getSearchScope(), this)); 574 } 575 try { 576 NamingEnumeration<SearchResult> results = dirContext.search(searchBaseDn, filterExpr, filterArgs, scts); 577 DocumentModelList entries = ldapResultsToDocumentModels(results, fetchReferences); 578 579 if (orderBy != null && !orderBy.isEmpty()) { 580 getDirectory().orderEntries(entries, orderBy); 581 } 582 return entries; 583 } catch (NameNotFoundException nnfe) { 584 // sometimes ActiveDirectory have some query fail with: LDAP: 585 // error code 32 - 0000208D: NameErr: DSID-031522C9, problem 586 // 2001 (NO_OBJECT). 587 // To keep the application usable return no results instead of 588 // crashing but log the error so that the AD admin 589 // can fix the issue. 590 log.error("Unexpected response from server while performing query: " + nnfe.getMessage(), nnfe); 591 return new DocumentModelListImpl(); 592 } 593 } catch (LimitExceededException e) { 594 throw new org.nuxeo.ecm.directory.SizeLimitExceededException(e); 595 } catch (NamingException e) { 596 throw new DirectoryException("executeQuery failed", e); 597 } 598 } 599 600 @Override 601 public DocumentModelList query(Map<String, Serializable> filter) throws DirectoryException { 602 // by default, do not fetch references of result entries 603 return query(filter, emptySet, new HashMap<String, String>()); 604 } 605 606 @Override 607 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy) 608 throws DirectoryException { 609 return query(filter, fulltext, false, orderBy); 610 } 611 612 @Override 613 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy, 614 boolean fetchReferences) throws DirectoryException { 615 return query(filter, fulltext, fetchReferences, orderBy); 616 } 617 618 @Override 619 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext) throws DirectoryException { 620 // by default, do not fetch references of result entries 621 return query(filter, fulltext, new HashMap<String, String>()); 622 } 623 624 @Override 625 public void close() throws DirectoryException { 626 try { 627 dirContext.close(); 628 } catch (NamingException e) { 629 throw new DirectoryException("close failed", e); 630 } finally { 631 getDirectory().removeSession(this); 632 } 633 } 634 635 @Override 636 public List<String> getProjection(Map<String, Serializable> filter, String columnName) throws DirectoryException { 637 return getProjection(filter, emptySet, columnName); 638 } 639 640 @Override 641 public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName) 642 throws DirectoryException { 643 // XXX: this suboptimal code should be either optimized for LDAP or 644 // moved to an abstract class 645 List<String> result = new ArrayList<String>(); 646 DocumentModelList docList = query(filter, fulltext); 647 String columnNameinDocModel = getDirectory().getFieldMapper().getDirectoryField(columnName); 648 for (DocumentModel docModel : docList) { 649 Object obj; 650 try { 651 obj = docModel.getProperty(schemaName, columnNameinDocModel); 652 } catch (PropertyException e) { 653 throw new DirectoryException(e); 654 } 655 String propValue; 656 if (obj instanceof String) { 657 propValue = (String) obj; 658 } else { 659 propValue = String.valueOf(obj); 660 } 661 result.add(propValue); 662 } 663 return result; 664 } 665 666 protected DocumentModel fieldMapToDocumentModel(Map<String, Object> fieldMap) throws DirectoryException { 667 String id = String.valueOf(fieldMap.get(getIdField())); 668 try { 669 DocumentModel docModel = BaseSession.createEntryModel(sid, schemaName, id, fieldMap, isReadOnly()); 670 EntryAdaptor adaptor = getDirectory().getDescriptor().getEntryAdaptor(); 671 if (adaptor != null) { 672 docModel = adaptor.adapt(directory, docModel); 673 } 674 return docModel; 675 } catch (PropertyException e) { 676 log.error(e, e); 677 return null; 678 } 679 } 680 681 @SuppressWarnings("unchecked") 682 protected Object getFieldValue(Attribute attribute, String fieldName, String entryId, boolean fetchReferences) 683 throws DirectoryException { 684 685 Field field = schemaFieldMap.get(fieldName); 686 Type type = field.getType(); 687 Object defaultValue = field.getDefaultValue(); 688 String typeName = type.getName(); 689 if (attribute == null) { 690 return defaultValue; 691 } 692 Object value; 693 try { 694 value = attribute.get(); 695 } catch (NamingException e) { 696 throw new DirectoryException("Could not fetch value for " + attribute, e); 697 } 698 if (value == null) { 699 return defaultValue; 700 } 701 String trimmedValue = value.toString().trim(); 702 if ("string".equals(typeName)) { 703 return trimmedValue; 704 } else if ("integer".equals(typeName) || "long".equals(typeName)) { 705 if ("".equals(trimmedValue)) { 706 return defaultValue; 707 } 708 try { 709 return Long.valueOf(trimmedValue); 710 } catch (NumberFormatException e) { 711 log.error(String.format( 712 "field %s of type %s has non-numeric value found on server: '%s' (ignoring and using default value instead)", 713 fieldName, typeName, trimmedValue)); 714 return defaultValue; 715 } 716 } else if (type.isListType()) { 717 List<String> parsedItems = new LinkedList<String>(); 718 NamingEnumeration<Object> values = null; 719 try { 720 values = (NamingEnumeration<Object>) attribute.getAll(); 721 while (values.hasMore()) { 722 parsedItems.add(values.next().toString().trim()); 723 } 724 return parsedItems; 725 } catch (NamingException e) { 726 log.error(String.format( 727 "field %s of type %s has non list value found on server: '%s' (ignoring and using default value instead)", 728 fieldName, typeName, values != null ? values.toString() : trimmedValue)); 729 return defaultValue; 730 } finally { 731 if (values != null) { 732 try { 733 values.close(); 734 } catch (NamingException e) { 735 log.error(e, e); 736 } 737 } 738 } 739 } else if ("date".equals(typeName)) { 740 if ("".equals(trimmedValue)) { 741 return defaultValue; 742 } 743 try { 744 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'Z'"); 745 dateFormat.setTimeZone(new SimpleTimeZone(0, "Z")); 746 Date date = dateFormat.parse(trimmedValue); 747 Calendar cal = Calendar.getInstance(); 748 cal.setTime(date); 749 return cal; 750 } catch (ParseException e) { 751 log.error(String.format( 752 "field %s of type %s has invalid value found on server: '%s' (ignoring and using default value instead)", 753 fieldName, typeName, trimmedValue)); 754 return defaultValue; 755 } 756 } else if ("content".equals(typeName)) { 757 return Blobs.createBlob((byte[]) value); 758 } else { 759 throw new DirectoryException("Field type not supported in directories: " + typeName); 760 } 761 } 762 763 @SuppressWarnings("unchecked") 764 protected Attribute getAttributeValue(String fieldName, Object value) throws DirectoryException { 765 Attribute attribute = new BasicAttribute(getDirectory().getFieldMapper().getBackendField(fieldName)); 766 Field field = schemaFieldMap.get(fieldName); 767 if (field == null) { 768 String message = String.format("Invalid field name '%s' for directory '%s' with schema '%s'", fieldName, 769 directory.getName(), directory.getSchema()); 770 throw new DirectoryException(message); 771 } 772 Type type = field.getType(); 773 String typeName = type.getName(); 774 775 if ("string".equals(typeName)) { 776 attribute.add(value); 777 } else if ("integer".equals(typeName) || "long".equals(typeName)) { 778 attribute.add(value.toString()); 779 } else if (type.isListType()) { 780 Collection<String> valueItems; 781 if (value instanceof String[]) { 782 valueItems = Arrays.asList((String[]) value); 783 } else if (value instanceof Collection) { 784 valueItems = (Collection<String>) value; 785 } else { 786 throw new DirectoryException(String.format("field %s with value %s does not match type %s", fieldName, 787 value.toString(), type.getName())); 788 } 789 for (String item : valueItems) { 790 attribute.add(item); 791 } 792 } else if ("date".equals(typeName)) { 793 Calendar cal = (Calendar) value; 794 Date date = cal.getTime(); 795 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'Z'"); 796 dateFormat.setTimeZone(new SimpleTimeZone(0, "Z")); 797 attribute.add(dateFormat.format(date)); 798 } else if ("content".equals(typeName)) { 799 try { 800 attribute.add(((Blob) value).getByteArray()); 801 } catch (IOException e) { 802 throw new DirectoryException("Failed to get ByteArray value", e); 803 } 804 } else { 805 throw new DirectoryException("Field type not supported in directories: " + typeName); 806 } 807 808 return attribute; 809 } 810 811 protected DocumentModelList ldapResultsToDocumentModels(NamingEnumeration<SearchResult> results, 812 boolean fetchReferences) throws DirectoryException, NamingException { 813 DocumentModelListImpl list = new DocumentModelListImpl(); 814 try { 815 while (results.hasMore()) { 816 SearchResult result = results.next(); 817 DocumentModel entry = ldapResultToDocumentModel(result, null, fetchReferences); 818 if (entry != null) { 819 list.add(entry); 820 } 821 } 822 } catch (SizeLimitExceededException e) { 823 if (list.isEmpty()) { 824 // the server did no send back the truncated results set, 825 // re-throw the exception to that the user interface can display 826 // the error message 827 throw e; 828 } 829 // mark the collect results as a truncated result list 830 log.debug("SizeLimitExceededException caught," + " return truncated results. Original message: " 831 + e.getMessage() + " explanation: " + e.getExplanation()); 832 list.setTotalSize(-2); 833 } finally { 834 results.close(); 835 } 836 log.debug("LDAP search returned " + list.size() + " results"); 837 return list; 838 } 839 840 protected DocumentModel ldapResultToDocumentModel(SearchResult result, String entryId, boolean fetchReferences) 841 throws DirectoryException, NamingException { 842 Attributes attributes = result.getAttributes(); 843 String passwordFieldId = getPasswordField(); 844 Map<String, Object> fieldMap = new HashMap<String, Object>(); 845 846 Attribute attribute = attributes.get(idAttribute); 847 // NXP-2461: check that id field is filled + NXP-2730: make sure that 848 // entry id is the one returned from LDAP 849 if (attribute != null) { 850 Object entry = attribute.get(); 851 if (entry != null) { 852 entryId = entry.toString(); 853 } 854 } 855 // NXP-7136 handle id case 856 entryId = changeEntryIdCase(entryId, idCase); 857 858 if (entryId == null) { 859 // don't bother 860 return null; 861 } 862 for (String fieldName : schemaFieldMap.keySet()) { 863 List<Reference> references = directory.getReferences(fieldName); 864 if (references != null && references.size() > 0) { 865 if (fetchReferences) { 866 Map<String, List<String>> referencedIdsMap = new HashMap<>(); 867 for (Reference reference : references) { 868 // reference resolution 869 List<String> referencedIds; 870 if (reference instanceof LDAPReference) { 871 // optim: use the current LDAPSession directly to 872 // provide the LDAP reference with the needed backend entries 873 LDAPReference ldapReference = (LDAPReference) reference; 874 referencedIds = ldapReference.getLdapTargetIds(attributes); 875 } else if (reference instanceof LDAPTreeReference) { 876 // TODO: optimize using the current LDAPSession 877 // directly to provide the LDAP reference with the 878 // needed backend entries (needs to implement getLdapTargetIds) 879 LDAPTreeReference ldapReference = (LDAPTreeReference) reference; 880 referencedIds = ldapReference.getTargetIdsForSource(entryId); 881 } else { 882 referencedIds = reference.getTargetIdsForSource(entryId); 883 } 884 referencedIds = new ArrayList<>(referencedIds); 885 Collections.sort(referencedIds); 886 if (referencedIdsMap.containsKey(fieldName)) { 887 referencedIdsMap.get(fieldName).addAll(referencedIds); 888 } else { 889 referencedIdsMap.put(fieldName, referencedIds); 890 } 891 } 892 fieldMap.put(fieldName, referencedIdsMap.get(fieldName)); 893 } 894 } else { 895 // manage directly stored fields 896 String attributeId = getDirectory().getFieldMapper().getBackendField(fieldName); 897 if (attributeId.equals(LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY)) { 898 // this is the special DN readonly attribute 899 try { 900 fieldMap.put(fieldName, result.getNameInNamespace()); 901 } catch (UnsupportedOperationException e) { 902 // ignore ApacheDS partial implementation when running 903 // in embedded mode 904 } 905 } else { 906 // this is a regular attribute 907 attribute = attributes.get(attributeId); 908 if (fieldName.equals(passwordFieldId)) { 909 // do not try to fetch the password attribute 910 continue; 911 } else { 912 fieldMap.put(fieldName, getFieldValue(attribute, fieldName, entryId, fetchReferences)); 913 } 914 } 915 } 916 } 917 // check if the idAttribute was returned from the search. If not 918 // set it anyway, maybe changing its case if it's a String instance 919 String fieldId = getDirectory().getFieldMapper().getDirectoryField(idAttribute); 920 Object obj = fieldMap.get(fieldId); 921 if (obj == null) { 922 fieldMap.put(fieldId, changeEntryIdCase(entryId, getDirectory().getDescriptor().getMissingIdFieldCase())); 923 } else if (obj instanceof String) { 924 fieldMap.put(fieldId, changeEntryIdCase((String) obj, idCase)); 925 } 926 return fieldMapToDocumentModel(fieldMap); 927 } 928 929 protected String changeEntryIdCase(String id, String idFieldCase) { 930 if (MISSING_ID_LOWER_CASE.equals(idFieldCase)) { 931 return id.toLowerCase(); 932 } else if (MISSING_ID_UPPER_CASE.equals(idFieldCase)) { 933 return id.toUpperCase(); 934 } 935 // returns the unchanged id 936 return id; 937 } 938 939 @Override 940 public boolean authenticate(String username, String password) throws DirectoryException { 941 942 if (password == null || "".equals(password.trim())) { 943 // never use anonymous bind as a way to authenticate a user in 944 // Nuxeo EP 945 return false; 946 } 947 948 // lookup the user: fetch its dn 949 SearchResult entry; 950 try { 951 entry = getLdapEntry(username); 952 } catch (NamingException e) { 953 throw new DirectoryException("failed to fetch the ldap entry for " + username, e); 954 } 955 if (entry == null) { 956 // no such user => authentication failed 957 return false; 958 } 959 String dn = entry.getNameInNamespace(); 960 Properties env = (Properties) getDirectory().getContextProperties().clone(); 961 env.put(Context.SECURITY_PRINCIPAL, dn); 962 env.put(Context.SECURITY_CREDENTIALS, password); 963 964 InitialLdapContext authenticationDirContext = null; 965 try { 966 // creating a context does a bind 967 log.debug(String.format("LDAP bind dn='%s'", dn)); 968 // noinspection ResultOfObjectAllocationIgnored 969 authenticationDirContext = new InitialLdapContext(env, null); 970 // force reconnection to prevent from using a previous connection 971 // with an obsolete password (after an user has changed his 972 // password) 973 authenticationDirContext.reconnect(null); 974 log.debug("Bind succeeded, authentication ok"); 975 return true; 976 } catch (NamingException e) { 977 log.debug("Bind failed: " + e.getMessage()); 978 // authentication failed 979 return false; 980 } finally { 981 try { 982 if (authenticationDirContext != null) { 983 authenticationDirContext.close(); 984 } 985 } catch (NamingException e) { 986 log.error("Error closing authentication context when biding dn " + dn, e); 987 return false; 988 } 989 } 990 } 991 992 @Override 993 public boolean isAuthenticating() throws DirectoryException { 994 String password = getPasswordField(); 995 return schemaFieldMap.containsKey(password); 996 } 997 998 public boolean rdnMatchesIdField() { 999 return getDirectory().getDescriptor().rdnAttribute.equals(idAttribute); 1000 } 1001 1002 @SuppressWarnings("unchecked") 1003 protected List<String> getMandatoryAttributes(Attribute objectClassesAttribute) throws DirectoryException { 1004 try { 1005 List<String> mandatoryAttributes = new ArrayList<String>(); 1006 1007 DirContext schema = dirContext.getSchema(""); 1008 List<String> objectClasses = new ArrayList<String>(); 1009 if (objectClassesAttribute == null) { 1010 // use the creation classes as reference schema for this entry 1011 objectClasses.addAll(Arrays.asList(getDirectory().getDescriptor().getCreationClasses())); 1012 } else { 1013 // introspec the objectClass definitions to find the mandatory 1014 // attributes for this entry 1015 NamingEnumeration<Object> values = null; 1016 try { 1017 values = (NamingEnumeration<Object>) objectClassesAttribute.getAll(); 1018 while (values.hasMore()) { 1019 objectClasses.add(values.next().toString().trim()); 1020 } 1021 } catch (NamingException e) { 1022 throw new DirectoryException(e); 1023 } finally { 1024 if (values != null) { 1025 values.close(); 1026 } 1027 } 1028 } 1029 objectClasses.remove("top"); 1030 for (String creationClass : objectClasses) { 1031 Attributes attributes = schema.getAttributes("ClassDefinition/" + creationClass); 1032 Attribute attribute = attributes.get("MUST"); 1033 if (attribute != null) { 1034 NamingEnumeration<String> values = (NamingEnumeration<String>) attribute.getAll(); 1035 try { 1036 while (values.hasMore()) { 1037 String value = values.next(); 1038 mandatoryAttributes.add(value); 1039 } 1040 } finally { 1041 values.close(); 1042 } 1043 } 1044 } 1045 return mandatoryAttributes; 1046 } catch (NamingException e) { 1047 throw new DirectoryException("getMandatoryAttributes failed", e); 1048 } 1049 } 1050 1051 protected List<String> getMandatoryAttributes() throws DirectoryException { 1052 return getMandatoryAttributes(null); 1053 } 1054 1055 @Override 1056 // useful for the log function 1057 public String toString() { 1058 return String.format("LDAPSession '%s' for directory %s", sid, directory.getName()); 1059 } 1060 1061 @Override 1062 public DocumentModel createEntry(DocumentModel entry) { 1063 Map<String, Object> fieldMap = entry.getProperties(directory.getSchema()); 1064 Map<String, Object> simpleNameFieldMap = new HashMap<String, Object>(); 1065 for (Map.Entry<String, Object> fieldEntry : fieldMap.entrySet()) { 1066 String fieldKey = fieldEntry.getKey(); 1067 if (fieldKey.contains(":")) { 1068 fieldKey = fieldKey.split(":")[1]; 1069 } 1070 simpleNameFieldMap.put(fieldKey, fieldEntry.getValue()); 1071 } 1072 return createEntry(simpleNameFieldMap); 1073 } 1074 1075}