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