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