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