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