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