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.SimpleTypeImpl; 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.DirectoryException; 073import org.nuxeo.ecm.directory.DirectoryFieldMapper; 074import org.nuxeo.ecm.directory.EntryAdaptor; 075import org.nuxeo.ecm.directory.EntrySource; 076import org.nuxeo.ecm.directory.PasswordHelper; 077import org.nuxeo.ecm.directory.Reference; 078import org.nuxeo.ecm.directory.BaseDirectoryDescriptor.SubstringMatchType; 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 // set to false for debugging 094 private static final boolean HIDE_PASSWORD_IN_LOGS = true; 095 096 protected final String schemaName; 097 098 protected final DirContext dirContext; 099 100 protected final String idAttribute; 101 102 protected final String idCase; 103 104 protected final String searchBaseDn; 105 106 protected final Set<String> emptySet = Collections.emptySet(); 107 108 protected final String sid; 109 110 protected final Map<String, Field> schemaFieldMap; 111 112 protected SubstringMatchType substringMatchType; 113 114 protected final String rdnAttribute; 115 116 protected final String rdnField; 117 118 protected final String passwordHashAlgorithm; 119 120 public LDAPSession(LDAPDirectory directory, DirContext dirContext) { 121 super(directory); 122 this.dirContext = LdapRetryHandler.wrap(dirContext, directory.getServer().getRetries()); 123 DirectoryFieldMapper fieldMapper = directory.getFieldMapper(); 124 idAttribute = fieldMapper.getBackendField(getIdField()); 125 LDAPDirectoryDescriptor descriptor = directory.getDescriptor(); 126 idCase = descriptor.getIdCase(); 127 schemaName = directory.getSchema(); 128 schemaFieldMap = directory.getSchemaFieldMap(); 129 sid = String.valueOf(SIDGenerator.next()); 130 searchBaseDn = descriptor.getSearchBaseDn(); 131 substringMatchType = descriptor.getSubstringMatchType(); 132 rdnAttribute = descriptor.getRdnAttribute(); 133 rdnField = directory.getFieldMapper().getDirectoryField(rdnAttribute); 134 passwordHashAlgorithm = descriptor.passwordHashAlgorithm; 135 permissions = descriptor.permissions; 136 } 137 138 @Override 139 public LDAPDirectory getDirectory() { 140 return (LDAPDirectory) directory; 141 } 142 143 public DirContext getContext() { 144 return dirContext; 145 } 146 147 @Override 148 @SuppressWarnings("unchecked") 149 public DocumentModel createEntry(Map<String, Object> fieldMap) { 150 checkPermission(SecurityConstants.WRITE); 151 LDAPDirectoryDescriptor descriptor = getDirectory().getDescriptor(); 152 List<String> referenceFieldList = new LinkedList<String>(); 153 try { 154 String dn = String.format("%s=%s,%s", rdnAttribute, fieldMap.get(rdnField), descriptor.getCreationBaseDn()); 155 Attributes attrs = new BasicAttributes(); 156 Attribute attr; 157 158 List<String> mandatoryAttributes = getMandatoryAttributes(); 159 for (String mandatoryAttribute : mandatoryAttributes) { 160 attr = new BasicAttribute(mandatoryAttribute); 161 attr.add(" "); 162 attrs.put(attr); 163 } 164 165 String[] creationClasses = descriptor.getCreationClasses(); 166 if (creationClasses.length != 0) { 167 attr = new BasicAttribute("objectclass"); 168 for (String creationClasse : creationClasses) { 169 attr.add(creationClasse); 170 } 171 attrs.put(attr); 172 } 173 174 for (String fieldId : fieldMap.keySet()) { 175 String backendFieldId = getDirectory().getFieldMapper().getBackendField(fieldId); 176 if (fieldId.equals(getPasswordField())) { 177 attr = new BasicAttribute(backendFieldId); 178 String password = (String) fieldMap.get(fieldId); 179 password = PasswordHelper.hashPassword(password, passwordHashAlgorithm); 180 attr.add(password); 181 attrs.put(attr); 182 } else if (getDirectory().isReference(fieldId)) { 183 List<Reference> references = directory.getReferences(fieldId); 184 if (references.size() > 1) { 185 // not supported 186 } else { 187 Reference reference = references.get(0); 188 if (reference instanceof LDAPReference) { 189 attr = new BasicAttribute(((LDAPReference) reference).getStaticAttributeId()); 190 attr.add(descriptor.getEmptyRefMarker()); 191 attrs.put(attr); 192 } 193 } 194 referenceFieldList.add(fieldId); 195 } else if (LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY.equals(backendFieldId)) { 196 // ignore special DN field 197 log.warn(String.format("field %s is mapped to read only DN field: ignored", fieldId)); 198 } else { 199 Object value = fieldMap.get(fieldId); 200 if ((value != null) && !value.equals("") && !Collections.emptyList().equals(value)) { 201 attrs.put(getAttributeValue(fieldId, value)); 202 } 203 } 204 } 205 206 if (log.isDebugEnabled()) { 207 Attributes logAttrs; 208 if (HIDE_PASSWORD_IN_LOGS && attrs.get(getPasswordField()) != null) { 209 logAttrs = (Attributes) attrs.clone(); 210 logAttrs.put(getPasswordField(), "********"); // hide password in logs 211 } else { 212 logAttrs = attrs; 213 } 214 String idField = getIdField(); 215 log.debug(String.format("LDAPSession.createEntry(%s=%s): LDAP bind dn='%s' attrs='%s' [%s]", idField, 216 fieldMap.get(idField), dn, logAttrs, this)); 217 } 218 dirContext.bind(dn, null, attrs); 219 220 for (String referenceFieldName : referenceFieldList) { 221 List<Reference> references = directory.getReferences(referenceFieldName); 222 if (references.size() > 1) { 223 // not supported 224 } else { 225 Reference reference = references.get(0); 226 List<String> targetIds = (List<String>) fieldMap.get(referenceFieldName); 227 reference.addLinks((String) fieldMap.get(getIdField()), targetIds); 228 } 229 } 230 String dnFieldName = getDirectory().getFieldMapper().getDirectoryField(LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY); 231 if (getDirectory().getSchemaFieldMap().containsKey(dnFieldName)) { 232 // add the DN special attribute to the fieldmap of the new 233 // entry 234 fieldMap.put(dnFieldName, dn); 235 } 236 getDirectory().invalidateCaches(); 237 return fieldMapToDocumentModel(fieldMap); 238 } catch (NamingException e) { 239 handleException(e, "createEntry failed"); 240 return null; 241 } 242 } 243 244 @Override 245 public DocumentModel getEntry(String id) throws DirectoryException { 246 return getEntry(id, true); 247 } 248 249 @Override 250 public DocumentModel getEntry(String id, boolean fetchReferences) throws DirectoryException { 251 if (!hasPermission(SecurityConstants.READ)) { 252 return null; 253 } 254 return directory.getCache().getEntry(id, this, fetchReferences); 255 } 256 257 @Override 258 public DocumentModel getEntryFromSource(String id, boolean fetchReferences) throws DirectoryException { 259 try { 260 SearchResult result = getLdapEntry(id, true); 261 if (result == null) { 262 return null; 263 } 264 return ldapResultToDocumentModel(result, id, fetchReferences); 265 } catch (NamingException e) { 266 throw new DirectoryException("getEntry failed: " + e.getMessage(), e); 267 } 268 } 269 270 @Override 271 public boolean hasEntry(String id) throws DirectoryException { 272 try { 273 // TODO: check directory cache first 274 return getLdapEntry(id) != null; 275 } catch (NamingException e) { 276 throw new DirectoryException("hasEntry failed: " + e.getMessage(), e); 277 } 278 } 279 280 protected SearchResult getLdapEntry(String id) throws NamingException, DirectoryException { 281 return getLdapEntry(id, false); 282 } 283 284 protected SearchResult getLdapEntry(String id, boolean fetchAllAttributes) throws NamingException { 285 if (StringUtils.isEmpty(id)) { 286 log.warn("The application should not " + "query for entries with an empty id " + "=> return no results"); 287 return null; 288 } 289 String filterExpr; 290 String baseFilter = getDirectory().getBaseFilter(); 291 if (baseFilter.startsWith("(")) { 292 filterExpr = String.format("(&(%s={0})%s)", idAttribute, baseFilter); 293 } else { 294 filterExpr = String.format("(&(%s={0})(%s))", idAttribute, baseFilter); 295 } 296 String[] filterArgs = { id }; 297 SearchControls scts = getDirectory().getSearchControls(fetchAllAttributes); 298 299 if (log.isDebugEnabled()) { 300 log.debug(String.format("LDAPSession.getLdapEntry(%s, %s): LDAP search base='%s' filter='%s' " 301 + " args='%s' scope='%s' [%s]", id, fetchAllAttributes, searchBaseDn, filterExpr, id, 302 scts.getSearchScope(), this)); 303 } 304 NamingEnumeration<SearchResult> results; 305 try { 306 results = dirContext.search(searchBaseDn, filterExpr, filterArgs, scts); 307 } catch (NameNotFoundException nnfe) { 308 // sometimes ActiveDirectory have some query fail with: LDAP: 309 // error code 32 - 0000208D: NameErr: DSID-031522C9, problem 310 // 2001 (NO_OBJECT). 311 // To keep the application usable return no results instead of 312 // crashing but log the error so that the AD admin 313 // can fix the issue. 314 log.error("Unexpected response from server while performing query: " + nnfe.getMessage(), nnfe); 315 return null; 316 } 317 318 if (!results.hasMore()) { 319 log.debug("Entry not found: " + id); 320 return null; 321 } 322 SearchResult result = results.next(); 323 try { 324 String dn = result.getNameInNamespace(); 325 if (results.hasMore()) { 326 result = results.next(); 327 String dn2 = result.getNameInNamespace(); 328 String msg = String.format("Unable to fetch entry for '%s': found more than one match," 329 + " for instance: '%s' and '%s'", id, dn, dn2); 330 log.error(msg); 331 // ignore entries that are ambiguous while giving enough info 332 // in the logs to let the LDAP admin be able to fix the issue 333 return null; 334 } 335 if (log.isDebugEnabled()) { 336 log.debug(String.format("LDAPSession.getLdapEntry(%s, %s): LDAP search base='%s' filter='%s' " 337 + " args='%s' scope='%s' => found: %s [%s]", id, fetchAllAttributes, searchBaseDn, filterExpr, 338 id, scts.getSearchScope(), dn, this)); 339 } 340 } catch (UnsupportedOperationException e) { 341 // ignore unsupported operation thrown by the Apache DS server in 342 // the tests in embedded mode 343 } 344 return result; 345 } 346 347 @Override 348 public DocumentModelList getEntries() throws DirectoryException { 349 if (!hasPermission(SecurityConstants.READ)) { 350 return new DocumentModelListImpl(); 351 } 352 try { 353 SearchControls scts = getDirectory().getSearchControls(true); 354 if (log.isDebugEnabled()) { 355 log.debug(String.format("LDAPSession.getEntries(): LDAP search base='%s' filter='%s' " 356 + " args=* scope=%s [%s]", searchBaseDn, getDirectory().getBaseFilter(), scts.getSearchScope(), this)); 357 } 358 NamingEnumeration<SearchResult> results = dirContext.search(searchBaseDn, getDirectory().getBaseFilter(), scts); 359 // skip reference fetching 360 return ldapResultsToDocumentModels(results, false); 361 } catch (SizeLimitExceededException e) { 362 throw new org.nuxeo.ecm.directory.SizeLimitExceededException(e); 363 } catch (NamingException e) { 364 throw new DirectoryException("getEntries failed", e); 365 } 366 } 367 368 @Override 369 @SuppressWarnings("unchecked") 370 public void updateEntry(DocumentModel docModel) { 371 checkPermission(SecurityConstants.WRITE); 372 List<String> updateList = new ArrayList<String>(); 373 List<String> referenceFieldList = new LinkedList<String>(); 374 375 try { 376 for (String fieldName : schemaFieldMap.keySet()) { 377 if (!docModel.getPropertyObject(schemaName, fieldName).isDirty()) { 378 continue; 379 } 380 if (getDirectory().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 = getDirectory().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 getDirectory().invalidateCaches(); 456 } 457 458 protected void handleException(Exception e, String message) { 459 LdapExceptionProcessor processor = getDirectory().getDescriptor().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 checkPermission(SecurityConstants.WRITE); 477 checkDeleteConstraints(id); 478 try { 479 for (String fieldName : schemaFieldMap.keySet()) { 480 if (getDirectory().isReference(fieldName)) { 481 List<Reference> references = directory.getReferences(fieldName); 482 if (references.size() > 1) { 483 // not supported 484 } else { 485 Reference reference = references.get(0); 486 reference.removeLinksForSource(id); 487 } 488 } 489 } 490 SearchResult result = getLdapEntry(id); 491 492 if (log.isDebugEnabled()) { 493 log.debug(String.format("LDAPSession.deleteEntry(%s): LDAP destroySubcontext dn='%s' [%s]", id, 494 result.getNameInNamespace(), this)); 495 } 496 dirContext.destroySubcontext(result.getNameInNamespace()); 497 } catch (NamingException e) { 498 handleException(e, "deleteEntry failed for: " + id); 499 } 500 getDirectory().invalidateCaches(); 501 } 502 503 @Override 504 public void deleteEntry(String id, Map<String, String> map) { 505 log.warn("Calling deleteEntry extended on LDAP directory"); 506 deleteEntry(id); 507 } 508 509 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, boolean fetchReferences, 510 Map<String, String> orderBy) throws DirectoryException { 511 if (!hasPermission(SecurityConstants.READ)) { 512 return new DocumentModelListImpl(); 513 } 514 try { 515 // building the query using filterExpr / filterArgs to 516 // escape special characters and to fulltext search only on 517 // the explicitly specified fields 518 String[] filters = new String[filter.size()]; 519 String[] filterArgs = new String[filter.size()]; 520 521 if (fulltext == null) { 522 fulltext = Collections.emptySet(); 523 } 524 525 int index = 0; 526 for (String fieldName : filter.keySet()) { 527 if (getDirectory().isReference(fieldName)) { 528 log.warn(fieldName + " is a reference and will be ignored as a query criterion"); 529 continue; 530 } 531 532 String backendFieldName = getDirectory().getFieldMapper().getBackendField(fieldName); 533 Object fieldValue = filter.get(fieldName); 534 535 StringBuilder currentFilter = new StringBuilder(); 536 currentFilter.append("("); 537 if (fieldValue == null) { 538 currentFilter.append("!(" + backendFieldName + "=*)"); 539 } else if ("".equals(fieldValue)) { 540 if (fulltext.contains(fieldName)) { 541 currentFilter.append(backendFieldName + "=*"); 542 } else { 543 currentFilter.append("!(" + backendFieldName + "=*)"); 544 } 545 } else { 546 currentFilter.append(backendFieldName + "="); 547 if (fulltext.contains(fieldName)) { 548 switch (substringMatchType) { 549 case subinitial: 550 currentFilter.append("{" + index + "}*"); 551 break; 552 case subfinal: 553 currentFilter.append("*{" + index + "}"); 554 break; 555 case subany: 556 currentFilter.append("*{" + index + "}*"); 557 break; 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 = "(&" + getDirectory().getBaseFilter() + StringUtils.join(filters) + ')'; 579 SearchControls scts = getDirectory().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 getDirectory().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 getDirectory().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 = getDirectory().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 = getDirectory().getDescriptor().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 if (type instanceof SimpleTypeImpl) { 699 // type with constraint 700 type = type.getSuperType(); 701 } 702 Object defaultValue = field.getDefaultValue(); 703 String typeName = type.getName(); 704 if (attribute == null) { 705 return defaultValue; 706 } 707 Object value; 708 try { 709 value = attribute.get(); 710 } catch (NamingException e) { 711 throw new DirectoryException("Could not fetch value for " + attribute, e); 712 } 713 if (value == null) { 714 return defaultValue; 715 } 716 String trimmedValue = value.toString().trim(); 717 if ("string".equals(typeName)) { 718 return trimmedValue; 719 } else if ("integer".equals(typeName) || "long".equals(typeName)) { 720 if ("".equals(trimmedValue)) { 721 return defaultValue; 722 } 723 try { 724 return Long.valueOf(trimmedValue); 725 } catch (NumberFormatException e) { 726 log.error(String.format( 727 "field %s of type %s has non-numeric value found on server: '%s' (ignoring and using default value instead)", 728 fieldName, typeName, trimmedValue)); 729 return defaultValue; 730 } 731 } else if (type.isListType()) { 732 List<String> parsedItems = new LinkedList<String>(); 733 NamingEnumeration<Object> values = null; 734 try { 735 values = (NamingEnumeration<Object>) attribute.getAll(); 736 while (values.hasMore()) { 737 parsedItems.add(values.next().toString().trim()); 738 } 739 return parsedItems; 740 } catch (NamingException e) { 741 log.error(String.format( 742 "field %s of type %s has non list value found on server: '%s' (ignoring and using default value instead)", 743 fieldName, typeName, values != null ? values.toString() : trimmedValue)); 744 return defaultValue; 745 } finally { 746 if (values != null) { 747 try { 748 values.close(); 749 } catch (NamingException e) { 750 log.error(e, e); 751 } 752 } 753 } 754 } else if ("date".equals(typeName)) { 755 if ("".equals(trimmedValue)) { 756 return defaultValue; 757 } 758 try { 759 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'Z'"); 760 dateFormat.setTimeZone(new SimpleTimeZone(0, "Z")); 761 Date date = dateFormat.parse(trimmedValue); 762 Calendar cal = Calendar.getInstance(); 763 cal.setTime(date); 764 return cal; 765 } catch (ParseException e) { 766 log.error(String.format( 767 "field %s of type %s has invalid value found on server: '%s' (ignoring and using default value instead)", 768 fieldName, typeName, trimmedValue)); 769 return defaultValue; 770 } 771 } else if ("content".equals(typeName)) { 772 return Blobs.createBlob((byte[]) value); 773 } else { 774 throw new DirectoryException("Field type not supported in directories: " + typeName); 775 } 776 } 777 778 @SuppressWarnings("unchecked") 779 protected Attribute getAttributeValue(String fieldName, Object value) throws DirectoryException { 780 Attribute attribute = new BasicAttribute(getDirectory().getFieldMapper().getBackendField(fieldName)); 781 Field field = schemaFieldMap.get(fieldName); 782 if (field == null) { 783 String message = String.format("Invalid field name '%s' for directory '%s' with schema '%s'", fieldName, 784 directory.getName(), directory.getSchema()); 785 throw new DirectoryException(message); 786 } 787 Type type = field.getType(); 788 String typeName = type.getName(); 789 790 if ("string".equals(typeName)) { 791 attribute.add(value); 792 } else if ("integer".equals(typeName) || "long".equals(typeName)) { 793 attribute.add(value.toString()); 794 } else if (type.isListType()) { 795 Collection<String> valueItems; 796 if (value instanceof String[]) { 797 valueItems = Arrays.asList((String[]) value); 798 } else if (value instanceof Collection) { 799 valueItems = (Collection<String>) value; 800 } else { 801 throw new DirectoryException(String.format("field %s with value %s does not match type %s", fieldName, 802 value.toString(), type.getName())); 803 } 804 for (String item : valueItems) { 805 attribute.add(item); 806 } 807 } else if ("date".equals(typeName)) { 808 Calendar cal = (Calendar) value; 809 Date date = cal.getTime(); 810 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'Z'"); 811 dateFormat.setTimeZone(new SimpleTimeZone(0, "Z")); 812 attribute.add(dateFormat.format(date)); 813 } else if ("content".equals(typeName)) { 814 try { 815 attribute.add(((Blob) value).getByteArray()); 816 } catch (IOException e) { 817 throw new DirectoryException("Failed to get ByteArray value", e); 818 } 819 } else { 820 throw new DirectoryException("Field type not supported in directories: " + typeName); 821 } 822 823 return attribute; 824 } 825 826 protected DocumentModelList ldapResultsToDocumentModels(NamingEnumeration<SearchResult> results, 827 boolean fetchReferences) throws DirectoryException, NamingException { 828 DocumentModelListImpl list = new DocumentModelListImpl(); 829 try { 830 while (results.hasMore()) { 831 SearchResult result = results.next(); 832 DocumentModel entry = ldapResultToDocumentModel(result, null, fetchReferences); 833 if (entry != null) { 834 list.add(entry); 835 } 836 } 837 } catch (SizeLimitExceededException e) { 838 if (list.isEmpty()) { 839 // the server did no send back the truncated results set, 840 // re-throw the exception to that the user interface can display 841 // the error message 842 throw e; 843 } 844 // mark the collect results as a truncated result list 845 log.debug("SizeLimitExceededException caught," + " return truncated results. Original message: " 846 + e.getMessage() + " explanation: " + e.getExplanation()); 847 list.setTotalSize(-2); 848 } finally { 849 results.close(); 850 } 851 log.debug("LDAP search returned " + list.size() + " results"); 852 return list; 853 } 854 855 protected DocumentModel ldapResultToDocumentModel(SearchResult result, String entryId, boolean fetchReferences) 856 throws DirectoryException, NamingException { 857 Attributes attributes = result.getAttributes(); 858 String passwordFieldId = getPasswordField(); 859 Map<String, Object> fieldMap = new HashMap<String, Object>(); 860 861 Attribute attribute = attributes.get(idAttribute); 862 // NXP-2461: check that id field is filled + NXP-2730: make sure that 863 // entry id is the one returned from LDAP 864 if (attribute != null) { 865 Object entry = attribute.get(); 866 if (entry != null) { 867 entryId = entry.toString(); 868 } 869 } 870 // NXP-7136 handle id case 871 entryId = changeEntryIdCase(entryId, idCase); 872 873 if (entryId == null) { 874 // don't bother 875 return null; 876 } 877 for (String fieldName : schemaFieldMap.keySet()) { 878 List<Reference> references = directory.getReferences(fieldName); 879 if (references != null && references.size() > 0) { 880 if (fetchReferences) { 881 Map<String, List<String>> referencedIdsMap = new HashMap<>(); 882 for (Reference reference : references) { 883 // reference resolution 884 List<String> referencedIds; 885 if (reference instanceof LDAPReference) { 886 // optim: use the current LDAPSession directly to 887 // provide the LDAP reference with the needed backend entries 888 LDAPReference ldapReference = (LDAPReference) reference; 889 referencedIds = ldapReference.getLdapTargetIds(attributes); 890 } else if (reference instanceof LDAPTreeReference) { 891 // TODO: optimize using the current LDAPSession 892 // directly to provide the LDAP reference with the 893 // needed backend entries (needs to implement getLdapTargetIds) 894 LDAPTreeReference ldapReference = (LDAPTreeReference) reference; 895 referencedIds = ldapReference.getTargetIdsForSource(entryId); 896 } else { 897 referencedIds = reference.getTargetIdsForSource(entryId); 898 } 899 referencedIds = new ArrayList<>(referencedIds); 900 Collections.sort(referencedIds); 901 if (referencedIdsMap.containsKey(fieldName)) { 902 referencedIdsMap.get(fieldName).addAll(referencedIds); 903 } else { 904 referencedIdsMap.put(fieldName, referencedIds); 905 } 906 } 907 fieldMap.put(fieldName, referencedIdsMap.get(fieldName)); 908 } 909 } else { 910 // manage directly stored fields 911 String attributeId = getDirectory().getFieldMapper().getBackendField(fieldName); 912 if (attributeId.equals(LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY)) { 913 // this is the special DN readonly attribute 914 try { 915 fieldMap.put(fieldName, result.getNameInNamespace()); 916 } catch (UnsupportedOperationException e) { 917 // ignore ApacheDS partial implementation when running 918 // in embedded mode 919 } 920 } else { 921 // this is a regular attribute 922 attribute = attributes.get(attributeId); 923 if (fieldName.equals(passwordFieldId)) { 924 // do not try to fetch the password attribute 925 continue; 926 } else { 927 fieldMap.put(fieldName, getFieldValue(attribute, fieldName, entryId, fetchReferences)); 928 } 929 } 930 } 931 } 932 // check if the idAttribute was returned from the search. If not 933 // set it anyway, maybe changing its case if it's a String instance 934 String fieldId = getDirectory().getFieldMapper().getDirectoryField(idAttribute); 935 Object obj = fieldMap.get(fieldId); 936 if (obj == null) { 937 fieldMap.put(fieldId, changeEntryIdCase(entryId, getDirectory().getDescriptor().getMissingIdFieldCase())); 938 } else if (obj instanceof String) { 939 fieldMap.put(fieldId, changeEntryIdCase((String) obj, idCase)); 940 } 941 return fieldMapToDocumentModel(fieldMap); 942 } 943 944 protected String changeEntryIdCase(String id, String idFieldCase) { 945 if (MISSING_ID_LOWER_CASE.equals(idFieldCase)) { 946 return id.toLowerCase(); 947 } else if (MISSING_ID_UPPER_CASE.equals(idFieldCase)) { 948 return id.toUpperCase(); 949 } 950 // returns the unchanged id 951 return id; 952 } 953 954 @Override 955 public boolean authenticate(String username, String password) throws DirectoryException { 956 957 if (password == null || "".equals(password.trim())) { 958 // never use anonymous bind as a way to authenticate a user in 959 // Nuxeo EP 960 return false; 961 } 962 963 // lookup the user: fetch its dn 964 SearchResult entry; 965 try { 966 entry = getLdapEntry(username); 967 } catch (NamingException e) { 968 throw new DirectoryException("failed to fetch the ldap entry for " + username, e); 969 } 970 if (entry == null) { 971 // no such user => authentication failed 972 return false; 973 } 974 String dn = entry.getNameInNamespace(); 975 Properties env = (Properties) getDirectory().getContextProperties().clone(); 976 env.put(Context.SECURITY_PRINCIPAL, dn); 977 env.put(Context.SECURITY_CREDENTIALS, password); 978 979 InitialLdapContext authenticationDirContext = null; 980 try { 981 // creating a context does a bind 982 log.debug(String.format("LDAP bind dn='%s'", dn)); 983 // noinspection ResultOfObjectAllocationIgnored 984 authenticationDirContext = new InitialLdapContext(env, null); 985 // force reconnection to prevent from using a previous connection 986 // with an obsolete password (after an user has changed his 987 // password) 988 authenticationDirContext.reconnect(null); 989 log.debug("Bind succeeded, authentication ok"); 990 return true; 991 } catch (NamingException e) { 992 log.debug("Bind failed: " + e.getMessage()); 993 // authentication failed 994 return false; 995 } finally { 996 try { 997 if (authenticationDirContext != null) { 998 authenticationDirContext.close(); 999 } 1000 } catch (NamingException e) { 1001 log.error("Error closing authentication context when biding dn " + dn, e); 1002 return false; 1003 } 1004 } 1005 } 1006 1007 @Override 1008 public boolean isAuthenticating() throws DirectoryException { 1009 String password = getPasswordField(); 1010 return schemaFieldMap.containsKey(password); 1011 } 1012 1013 public boolean rdnMatchesIdField() { 1014 return getDirectory().getDescriptor().rdnAttribute.equals(idAttribute); 1015 } 1016 1017 @SuppressWarnings("unchecked") 1018 protected List<String> getMandatoryAttributes(Attribute objectClassesAttribute) throws DirectoryException { 1019 try { 1020 List<String> mandatoryAttributes = new ArrayList<String>(); 1021 1022 DirContext schema = dirContext.getSchema(""); 1023 List<String> objectClasses = new ArrayList<String>(); 1024 if (objectClassesAttribute == null) { 1025 // use the creation classes as reference schema for this entry 1026 objectClasses.addAll(Arrays.asList(getDirectory().getDescriptor().getCreationClasses())); 1027 } else { 1028 // introspec the objectClass definitions to find the mandatory 1029 // attributes for this entry 1030 NamingEnumeration<Object> values = null; 1031 try { 1032 values = (NamingEnumeration<Object>) objectClassesAttribute.getAll(); 1033 while (values.hasMore()) { 1034 objectClasses.add(values.next().toString().trim()); 1035 } 1036 } catch (NamingException e) { 1037 throw new DirectoryException(e); 1038 } finally { 1039 if (values != null) { 1040 values.close(); 1041 } 1042 } 1043 } 1044 objectClasses.remove("top"); 1045 for (String creationClass : objectClasses) { 1046 Attributes attributes = schema.getAttributes("ClassDefinition/" + creationClass); 1047 Attribute attribute = attributes.get("MUST"); 1048 if (attribute != null) { 1049 NamingEnumeration<String> values = (NamingEnumeration<String>) attribute.getAll(); 1050 try { 1051 while (values.hasMore()) { 1052 String value = values.next(); 1053 mandatoryAttributes.add(value); 1054 } 1055 } finally { 1056 values.close(); 1057 } 1058 } 1059 } 1060 return mandatoryAttributes; 1061 } catch (NamingException e) { 1062 throw new DirectoryException("getMandatoryAttributes failed", e); 1063 } 1064 } 1065 1066 protected List<String> getMandatoryAttributes() throws DirectoryException { 1067 return getMandatoryAttributes(null); 1068 } 1069 1070 @Override 1071 // useful for the log function 1072 public String toString() { 1073 return String.format("LDAPSession '%s' for directory %s", sid, directory.getName()); 1074 } 1075 1076 @Override 1077 public DocumentModel createEntry(DocumentModel entry) { 1078 Map<String, Object> fieldMap = entry.getProperties(directory.getSchema()); 1079 Map<String, Object> simpleNameFieldMap = new HashMap<String, Object>(); 1080 for (Map.Entry<String, Object> fieldEntry : fieldMap.entrySet()) { 1081 String fieldKey = fieldEntry.getKey(); 1082 if (fieldKey.contains(":")) { 1083 fieldKey = fieldKey.split(":")[1]; 1084 } 1085 simpleNameFieldMap.put(fieldKey, fieldEntry.getValue()); 1086 } 1087 return createEntry(simpleNameFieldMap); 1088 } 1089 1090}