/*
* (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*
*/
package org.nuxeo.ecm.directory.ldap;
import java.io.IOException;
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SimpleTimeZone;
import javax.naming.Context;
import javax.naming.LimitExceededException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.SizeLimitExceededException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.core.api.RecoverableClientException;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.schema.types.Field;
import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl;
import org.nuxeo.ecm.core.schema.types.Type;
import org.nuxeo.ecm.core.utils.SIDGenerator;
import org.nuxeo.ecm.directory.BaseSession;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.DirectoryFieldMapper;
import org.nuxeo.ecm.directory.EntryAdaptor;
import org.nuxeo.ecm.directory.EntrySource;
import org.nuxeo.ecm.directory.PasswordHelper;
import org.nuxeo.ecm.directory.Reference;
import org.nuxeo.ecm.directory.BaseDirectoryDescriptor.SubstringMatchType;
/**
* This class represents a session against an LDAPDirectory.
*
* @author Olivier Grisel <ogrisel@nuxeo.com>
*/
public class LDAPSession extends BaseSession implements EntrySource {
protected static final String MISSING_ID_LOWER_CASE = "lower";
protected static final String MISSING_ID_UPPER_CASE = "upper";
private static final Log log = LogFactory.getLog(LDAPSession.class);
// set to false for debugging
private static final boolean HIDE_PASSWORD_IN_LOGS = true;
protected final String schemaName;
protected final DirContext dirContext;
protected final String idAttribute;
protected final String idCase;
protected final String searchBaseDn;
protected final Set<String> emptySet = Collections.emptySet();
protected final String sid;
protected final Map<String, Field> schemaFieldMap;
protected SubstringMatchType substringMatchType;
protected final String rdnAttribute;
protected final String rdnField;
protected final String passwordHashAlgorithm;
public LDAPSession(LDAPDirectory directory, DirContext dirContext) {
super(directory);
this.dirContext = LdapRetryHandler.wrap(dirContext, directory.getServer().getRetries());
DirectoryFieldMapper fieldMapper = directory.getFieldMapper();
idAttribute = fieldMapper.getBackendField(getIdField());
LDAPDirectoryDescriptor descriptor = directory.getDescriptor();
idCase = descriptor.getIdCase();
schemaName = directory.getSchema();
schemaFieldMap = directory.getSchemaFieldMap();
sid = String.valueOf(SIDGenerator.next());
searchBaseDn = descriptor.getSearchBaseDn();
substringMatchType = descriptor.getSubstringMatchType();
rdnAttribute = descriptor.getRdnAttribute();
rdnField = directory.getFieldMapper().getDirectoryField(rdnAttribute);
passwordHashAlgorithm = descriptor.passwordHashAlgorithm;
permissions = descriptor.permissions;
}
@Override
public LDAPDirectory getDirectory() {
return (LDAPDirectory) directory;
}
public DirContext getContext() {
return dirContext;
}
@Override
@SuppressWarnings("unchecked")
public DocumentModel createEntry(Map<String, Object> fieldMap) {
checkPermission(SecurityConstants.WRITE);
LDAPDirectoryDescriptor descriptor = getDirectory().getDescriptor();
List<String> referenceFieldList = new LinkedList<String>();
try {
String dn = String.format("%s=%s,%s", rdnAttribute, fieldMap.get(rdnField), descriptor.getCreationBaseDn());
Attributes attrs = new BasicAttributes();
Attribute attr;
List<String> mandatoryAttributes = getMandatoryAttributes();
for (String mandatoryAttribute : mandatoryAttributes) {
attr = new BasicAttribute(mandatoryAttribute);
attr.add(" ");
attrs.put(attr);
}
String[] creationClasses = descriptor.getCreationClasses();
if (creationClasses.length != 0) {
attr = new BasicAttribute("objectclass");
for (String creationClasse : creationClasses) {
attr.add(creationClasse);
}
attrs.put(attr);
}
for (String fieldId : fieldMap.keySet()) {
String backendFieldId = getDirectory().getFieldMapper().getBackendField(fieldId);
if (fieldId.equals(getPasswordField())) {
attr = new BasicAttribute(backendFieldId);
String password = (String) fieldMap.get(fieldId);
password = PasswordHelper.hashPassword(password, passwordHashAlgorithm);
attr.add(password);
attrs.put(attr);
} else if (getDirectory().isReference(fieldId)) {
List<Reference> references = directory.getReferences(fieldId);
if (references.size() > 1) {
// not supported
} else {
Reference reference = references.get(0);
if (reference instanceof LDAPReference) {
attr = new BasicAttribute(((LDAPReference) reference).getStaticAttributeId());
attr.add(descriptor.getEmptyRefMarker());
attrs.put(attr);
}
}
referenceFieldList.add(fieldId);
} else if (LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY.equals(backendFieldId)) {
// ignore special DN field
log.warn(String.format("field %s is mapped to read only DN field: ignored", fieldId));
} else {
Object value = fieldMap.get(fieldId);
if ((value != null) && !value.equals("") && !Collections.emptyList().equals(value)) {
attrs.put(getAttributeValue(fieldId, value));
}
}
}
if (log.isDebugEnabled()) {
Attributes logAttrs;
if (HIDE_PASSWORD_IN_LOGS && attrs.get(getPasswordField()) != null) {
logAttrs = (Attributes) attrs.clone();
logAttrs.put(getPasswordField(), "********"); // hide password in logs
} else {
logAttrs = attrs;
}
String idField = getIdField();
log.debug(String.format("LDAPSession.createEntry(%s=%s): LDAP bind dn='%s' attrs='%s' [%s]", idField,
fieldMap.get(idField), dn, logAttrs, this));
}
dirContext.bind(dn, null, attrs);
for (String referenceFieldName : referenceFieldList) {
List<Reference> references = directory.getReferences(referenceFieldName);
if (references.size() > 1) {
// not supported
} else {
Reference reference = references.get(0);
List<String> targetIds = (List<String>) fieldMap.get(referenceFieldName);
reference.addLinks((String) fieldMap.get(getIdField()), targetIds);
}
}
String dnFieldName = getDirectory().getFieldMapper().getDirectoryField(LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY);
if (getDirectory().getSchemaFieldMap().containsKey(dnFieldName)) {
// add the DN special attribute to the fieldmap of the new
// entry
fieldMap.put(dnFieldName, dn);
}
getDirectory().invalidateCaches();
return fieldMapToDocumentModel(fieldMap);
} catch (NamingException e) {
handleException(e, "createEntry failed");
return null;
}
}
@Override
public DocumentModel getEntry(String id) throws DirectoryException {
return getEntry(id, true);
}
@Override
public DocumentModel getEntry(String id, boolean fetchReferences) throws DirectoryException {
if (!hasPermission(SecurityConstants.READ)) {
return null;
}
return directory.getCache().getEntry(id, this, fetchReferences);
}
@Override
public DocumentModel getEntryFromSource(String id, boolean fetchReferences) throws DirectoryException {
try {
SearchResult result = getLdapEntry(id, true);
if (result == null) {
return null;
}
return ldapResultToDocumentModel(result, id, fetchReferences);
} catch (NamingException e) {
throw new DirectoryException("getEntry failed: " + e.getMessage(), e);
}
}
@Override
public boolean hasEntry(String id) throws DirectoryException {
try {
// TODO: check directory cache first
return getLdapEntry(id) != null;
} catch (NamingException e) {
throw new DirectoryException("hasEntry failed: " + e.getMessage(), e);
}
}
protected SearchResult getLdapEntry(String id) throws NamingException, DirectoryException {
return getLdapEntry(id, false);
}
protected SearchResult getLdapEntry(String id, boolean fetchAllAttributes) throws NamingException {
if (StringUtils.isEmpty(id)) {
log.warn("The application should not " + "query for entries with an empty id " + "=> return no results");
return null;
}
String filterExpr;
String baseFilter = getDirectory().getBaseFilter();
if (baseFilter.startsWith("(")) {
filterExpr = String.format("(&(%s={0})%s)", idAttribute, baseFilter);
} else {
filterExpr = String.format("(&(%s={0})(%s))", idAttribute, baseFilter);
}
String[] filterArgs = { id };
SearchControls scts = getDirectory().getSearchControls(fetchAllAttributes);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPSession.getLdapEntry(%s, %s): LDAP search base='%s' filter='%s' "
+ " args='%s' scope='%s' [%s]", id, fetchAllAttributes, searchBaseDn, filterExpr, id,
scts.getSearchScope(), this));
}
NamingEnumeration<SearchResult> results;
try {
results = dirContext.search(searchBaseDn, filterExpr, filterArgs, scts);
} catch (NameNotFoundException nnfe) {
// sometimes ActiveDirectory have some query fail with: LDAP:
// error code 32 - 0000208D: NameErr: DSID-031522C9, problem
// 2001 (NO_OBJECT).
// To keep the application usable return no results instead of
// crashing but log the error so that the AD admin
// can fix the issue.
log.error("Unexpected response from server while performing query: " + nnfe.getMessage(), nnfe);
return null;
}
if (!results.hasMore()) {
log.debug("Entry not found: " + id);
return null;
}
SearchResult result = results.next();
try {
String dn = result.getNameInNamespace();
if (results.hasMore()) {
result = results.next();
String dn2 = result.getNameInNamespace();
String msg = String.format("Unable to fetch entry for '%s': found more than one match,"
+ " for instance: '%s' and '%s'", id, dn, dn2);
log.error(msg);
// ignore entries that are ambiguous while giving enough info
// in the logs to let the LDAP admin be able to fix the issue
return null;
}
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPSession.getLdapEntry(%s, %s): LDAP search base='%s' filter='%s' "
+ " args='%s' scope='%s' => found: %s [%s]", id, fetchAllAttributes, searchBaseDn, filterExpr,
id, scts.getSearchScope(), dn, this));
}
} catch (UnsupportedOperationException e) {
// ignore unsupported operation thrown by the Apache DS server in
// the tests in embedded mode
}
return result;
}
@Override
public DocumentModelList getEntries() throws DirectoryException {
if (!hasPermission(SecurityConstants.READ)) {
return new DocumentModelListImpl();
}
try {
SearchControls scts = getDirectory().getSearchControls(true);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPSession.getEntries(): LDAP search base='%s' filter='%s' "
+ " args=* scope=%s [%s]", searchBaseDn, getDirectory().getBaseFilter(), scts.getSearchScope(), this));
}
NamingEnumeration<SearchResult> results = dirContext.search(searchBaseDn, getDirectory().getBaseFilter(), scts);
// skip reference fetching
return ldapResultsToDocumentModels(results, false);
} catch (SizeLimitExceededException e) {
throw new org.nuxeo.ecm.directory.SizeLimitExceededException(e);
} catch (NamingException e) {
throw new DirectoryException("getEntries failed", e);
}
}
@Override
@SuppressWarnings("unchecked")
public void updateEntry(DocumentModel docModel) {
checkPermission(SecurityConstants.WRITE);
List<String> updateList = new ArrayList<String>();
List<String> referenceFieldList = new LinkedList<String>();
try {
for (String fieldName : schemaFieldMap.keySet()) {
if (!docModel.getPropertyObject(schemaName, fieldName).isDirty()) {
continue;
}
if (getDirectory().isReference(fieldName)) {
referenceFieldList.add(fieldName);
} else {
updateList.add(fieldName);
}
}
if (!isReadOnlyEntry(docModel) && !updateList.isEmpty()) {
Attributes attrs = new BasicAttributes();
SearchResult ldapEntry = getLdapEntry(docModel.getId());
if (ldapEntry == null) {
throw new DirectoryException(docModel.getId() + " not found");
}
Attributes oldattrs = ldapEntry.getAttributes();
String dn = ldapEntry.getNameInNamespace();
Attributes attrsToDel = new BasicAttributes();
for (String f : updateList) {
Object value = docModel.getProperty(schemaName, f);
String backendField = getDirectory().getFieldMapper().getBackendField(f);
if (LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY.equals(backendField)) {
// skip special LDAP DN field that is readonly
log.warn(String.format("field %s is mapped to read only DN field: ignored", f));
continue;
}
if (value == null || value.equals("")) {
Attribute objectClasses = oldattrs.get("objectClass");
Attribute attr;
if (getMandatoryAttributes(objectClasses).contains(backendField)) {
attr = new BasicAttribute(backendField);
// XXX: this might fail if the mandatory attribute
// is typed integer for instance
attr.add(" ");
attrs.put(attr);
} else if (oldattrs.get(backendField) != null) {
attr = new BasicAttribute(backendField);
attr.add(oldattrs.get(backendField).get());
attrsToDel.put(attr);
}
} else if (f.equals(getPasswordField())) {
// The password has been updated, it has to be encrypted
Attribute attr = new BasicAttribute(backendField);
attr.add(PasswordHelper.hashPassword((String) value, passwordHashAlgorithm));
attrs.put(attr);
} else {
attrs.put(getAttributeValue(f, value));
}
}
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPSession.updateEntry(%s): LDAP modifyAttributes dn='%s' "
+ "mod_op='REMOVE_ATTRIBUTE' attr='%s' [%s]", docModel, dn, attrsToDel, this));
}
dirContext.modifyAttributes(dn, DirContext.REMOVE_ATTRIBUTE, attrsToDel);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPSession.updateEntry(%s): LDAP modifyAttributes dn='%s' "
+ "mod_op='REPLACE_ATTRIBUTE' attr='%s' [%s]", docModel, dn, attrs, this));
}
dirContext.modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, attrs);
}
// update reference fields
for (String referenceFieldName : referenceFieldList) {
List<Reference> references = directory.getReferences(referenceFieldName);
if (references.size() > 1) {
// not supported
} else {
Reference reference = references.get(0);
List<String> targetIds = (List<String>) docModel.getProperty(schemaName, referenceFieldName);
reference.setTargetIdsForSource(docModel.getId(), targetIds);
}
}
} catch (NamingException e) {
handleException(e, "updateEntry failed:");
}
getDirectory().invalidateCaches();
}
protected void handleException(Exception e, String message) {
LdapExceptionProcessor processor = getDirectory().getDescriptor().getExceptionProcessor();
RecoverableClientException userException = processor.extractRecoverableException(e);
if (userException != null) {
throw userException;
}
throw new DirectoryException(message + " " + e.getMessage(), e);
}
@Override
public void deleteEntry(DocumentModel dm) {
deleteEntry(dm.getId());
}
@Override
public void deleteEntry(String id) {
checkPermission(SecurityConstants.WRITE);
checkDeleteConstraints(id);
try {
for (String fieldName : schemaFieldMap.keySet()) {
if (getDirectory().isReference(fieldName)) {
List<Reference> references = directory.getReferences(fieldName);
if (references.size() > 1) {
// not supported
} else {
Reference reference = references.get(0);
reference.removeLinksForSource(id);
}
}
}
SearchResult result = getLdapEntry(id);
if (log.isDebugEnabled()) {
log.debug(String.format("LDAPSession.deleteEntry(%s): LDAP destroySubcontext dn='%s' [%s]", id,
result.getNameInNamespace(), this));
}
dirContext.destroySubcontext(result.getNameInNamespace());
} catch (NamingException e) {
handleException(e, "deleteEntry failed for: " + id);
}
getDirectory().invalidateCaches();
}
@Override
public void deleteEntry(String id, Map<String, String> map) {
log.warn("Calling deleteEntry extended on LDAP directory");
deleteEntry(id);
}
public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, boolean fetchReferences,
Map<String, String> orderBy) throws DirectoryException {
if (!hasPermission(SecurityConstants.READ)) {
return new DocumentModelListImpl();
}
try {
// building the query using filterExpr / filterArgs to
// escape special characters and to fulltext search only on
// the explicitly specified fields
String[] filters = new String[filter.size()];
String[] filterArgs = new String[filter.size()];
if (fulltext == null) {
fulltext = Collections.emptySet();
}
int index = 0;
for (String fieldName : filter.keySet()) {
if (getDirectory().isReference(fieldName)) {
log.warn(fieldName + " is a reference and will be ignored as a query criterion");
continue;
}
String backendFieldName = getDirectory().getFieldMapper().getBackendField(fieldName);
Object fieldValue = filter.get(fieldName);
StringBuilder currentFilter = new StringBuilder();
currentFilter.append("(");
if (fieldValue == null) {
currentFilter.append("!(" + backendFieldName + "=*)");
} else if ("".equals(fieldValue)) {
if (fulltext.contains(fieldName)) {
currentFilter.append(backendFieldName + "=*");
} else {
currentFilter.append("!(" + backendFieldName + "=*)");
}
} else {
currentFilter.append(backendFieldName + "=");
if (fulltext.contains(fieldName)) {
switch (substringMatchType) {
case subinitial:
currentFilter.append("{" + index + "}*");
break;
case subfinal:
currentFilter.append("*{" + index + "}");
break;
case subany:
currentFilter.append("*{" + index + "}*");
break;
}
} else {
currentFilter.append("{" + index + "}");
}
}
currentFilter.append(")");
filters[index] = currentFilter.toString();
if (fieldValue != null && !"".equals(fieldValue)) {
if (fieldValue instanceof Blob) {
// filter arg could be a sequence of \xx where xx is the
// hexadecimal value of the byte
log.warn("Binary search is not supported");
} else {
// XXX: what kind of Objects can we get here? Is
// toString() enough?
filterArgs[index] = fieldValue.toString();
}
}
index++;
}
String filterExpr = "(&" + getDirectory().getBaseFilter() + StringUtils.join(filters) + ')';
SearchControls scts = getDirectory().getSearchControls(true);
if (log.isDebugEnabled()) {
log.debug(String.format(
"LDAPSession.query(...): LDAP search base='%s' filter='%s' args='%s' scope='%s' [%s]",
searchBaseDn, filterExpr, StringUtils.join(filterArgs, ","), scts.getSearchScope(), this));
}
try {
NamingEnumeration<SearchResult> results = dirContext.search(searchBaseDn, filterExpr, filterArgs, scts);
DocumentModelList entries = ldapResultsToDocumentModels(results, fetchReferences);
if (orderBy != null && !orderBy.isEmpty()) {
getDirectory().orderEntries(entries, orderBy);
}
return entries;
} catch (NameNotFoundException nnfe) {
// sometimes ActiveDirectory have some query fail with: LDAP:
// error code 32 - 0000208D: NameErr: DSID-031522C9, problem
// 2001 (NO_OBJECT).
// To keep the application usable return no results instead of
// crashing but log the error so that the AD admin
// can fix the issue.
log.error("Unexpected response from server while performing query: " + nnfe.getMessage(), nnfe);
return new DocumentModelListImpl();
}
} catch (LimitExceededException e) {
throw new org.nuxeo.ecm.directory.SizeLimitExceededException(e);
} catch (NamingException e) {
throw new DirectoryException("executeQuery failed", e);
}
}
@Override
public DocumentModelList query(Map<String, Serializable> filter) throws DirectoryException {
// by default, do not fetch references of result entries
return query(filter, emptySet, new HashMap<String, String>());
}
@Override
public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy)
throws DirectoryException {
return query(filter, fulltext, false, orderBy);
}
@Override
public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
boolean fetchReferences) throws DirectoryException {
return query(filter, fulltext, fetchReferences, orderBy);
}
@Override
public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext) throws DirectoryException {
// by default, do not fetch references of result entries
return query(filter, fulltext, new HashMap<String, String>());
}
@Override
public void close() throws DirectoryException {
try {
dirContext.close();
} catch (NamingException e) {
throw new DirectoryException("close failed", e);
} finally {
getDirectory().removeSession(this);
}
}
@Override
public List<String> getProjection(Map<String, Serializable> filter, String columnName) throws DirectoryException {
return getProjection(filter, emptySet, columnName);
}
@Override
public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName)
throws DirectoryException {
// XXX: this suboptimal code should be either optimized for LDAP or
// moved to an abstract class
List<String> result = new ArrayList<String>();
DocumentModelList docList = query(filter, fulltext);
String columnNameinDocModel = getDirectory().getFieldMapper().getDirectoryField(columnName);
for (DocumentModel docModel : docList) {
Object obj;
try {
obj = docModel.getProperty(schemaName, columnNameinDocModel);
} catch (PropertyException e) {
throw new DirectoryException(e);
}
String propValue;
if (obj instanceof String) {
propValue = (String) obj;
} else {
propValue = String.valueOf(obj);
}
result.add(propValue);
}
return result;
}
protected DocumentModel fieldMapToDocumentModel(Map<String, Object> fieldMap) throws DirectoryException {
String id = String.valueOf(fieldMap.get(getIdField()));
try {
DocumentModel docModel = BaseSession.createEntryModel(sid, schemaName, id, fieldMap, isReadOnly());
EntryAdaptor adaptor = getDirectory().getDescriptor().getEntryAdaptor();
if (adaptor != null) {
docModel = adaptor.adapt(directory, docModel);
}
return docModel;
} catch (PropertyException e) {
log.error(e, e);
return null;
}
}
@SuppressWarnings("unchecked")
protected Object getFieldValue(Attribute attribute, String fieldName, String entryId, boolean fetchReferences)
throws DirectoryException {
Field field = schemaFieldMap.get(fieldName);
Type type = field.getType();
if (type instanceof SimpleTypeImpl) {
// type with constraint
type = type.getSuperType();
}
Object defaultValue = field.getDefaultValue();
String typeName = type.getName();
if (attribute == null) {
return defaultValue;
}
Object value;
try {
value = attribute.get();
} catch (NamingException e) {
throw new DirectoryException("Could not fetch value for " + attribute, e);
}
if (value == null) {
return defaultValue;
}
String trimmedValue = value.toString().trim();
if ("string".equals(typeName)) {
return trimmedValue;
} else if ("integer".equals(typeName) || "long".equals(typeName)) {
if ("".equals(trimmedValue)) {
return defaultValue;
}
try {
return Long.valueOf(trimmedValue);
} catch (NumberFormatException e) {
log.error(String.format(
"field %s of type %s has non-numeric value found on server: '%s' (ignoring and using default value instead)",
fieldName, typeName, trimmedValue));
return defaultValue;
}
} else if (type.isListType()) {
List<String> parsedItems = new LinkedList<String>();
NamingEnumeration<Object> values = null;
try {
values = (NamingEnumeration<Object>) attribute.getAll();
while (values.hasMore()) {
parsedItems.add(values.next().toString().trim());
}
return parsedItems;
} catch (NamingException e) {
log.error(String.format(
"field %s of type %s has non list value found on server: '%s' (ignoring and using default value instead)",
fieldName, typeName, values != null ? values.toString() : trimmedValue));
return defaultValue;
} finally {
if (values != null) {
try {
values.close();
} catch (NamingException e) {
log.error(e, e);
}
}
}
} else if ("date".equals(typeName)) {
if ("".equals(trimmedValue)) {
return defaultValue;
}
try {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
dateFormat.setTimeZone(new SimpleTimeZone(0, "Z"));
Date date = dateFormat.parse(trimmedValue);
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal;
} catch (ParseException e) {
log.error(String.format(
"field %s of type %s has invalid value found on server: '%s' (ignoring and using default value instead)",
fieldName, typeName, trimmedValue));
return defaultValue;
}
} else if ("content".equals(typeName)) {
return Blobs.createBlob((byte[]) value);
} else {
throw new DirectoryException("Field type not supported in directories: " + typeName);
}
}
@SuppressWarnings("unchecked")
protected Attribute getAttributeValue(String fieldName, Object value) throws DirectoryException {
Attribute attribute = new BasicAttribute(getDirectory().getFieldMapper().getBackendField(fieldName));
Field field = schemaFieldMap.get(fieldName);
if (field == null) {
String message = String.format("Invalid field name '%s' for directory '%s' with schema '%s'", fieldName,
directory.getName(), directory.getSchema());
throw new DirectoryException(message);
}
Type type = field.getType();
String typeName = type.getName();
if ("string".equals(typeName)) {
attribute.add(value);
} else if ("integer".equals(typeName) || "long".equals(typeName)) {
attribute.add(value.toString());
} else if (type.isListType()) {
Collection<String> valueItems;
if (value instanceof String[]) {
valueItems = Arrays.asList((String[]) value);
} else if (value instanceof Collection) {
valueItems = (Collection<String>) value;
} else {
throw new DirectoryException(String.format("field %s with value %s does not match type %s", fieldName,
value.toString(), type.getName()));
}
for (String item : valueItems) {
attribute.add(item);
}
} else if ("date".equals(typeName)) {
Calendar cal = (Calendar) value;
Date date = cal.getTime();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss'Z'");
dateFormat.setTimeZone(new SimpleTimeZone(0, "Z"));
attribute.add(dateFormat.format(date));
} else if ("content".equals(typeName)) {
try {
attribute.add(((Blob) value).getByteArray());
} catch (IOException e) {
throw new DirectoryException("Failed to get ByteArray value", e);
}
} else {
throw new DirectoryException("Field type not supported in directories: " + typeName);
}
return attribute;
}
protected DocumentModelList ldapResultsToDocumentModels(NamingEnumeration<SearchResult> results,
boolean fetchReferences) throws DirectoryException, NamingException {
DocumentModelListImpl list = new DocumentModelListImpl();
try {
while (results.hasMore()) {
SearchResult result = results.next();
DocumentModel entry = ldapResultToDocumentModel(result, null, fetchReferences);
if (entry != null) {
list.add(entry);
}
}
} catch (SizeLimitExceededException e) {
if (list.isEmpty()) {
// the server did no send back the truncated results set,
// re-throw the exception to that the user interface can display
// the error message
throw e;
}
// mark the collect results as a truncated result list
log.debug("SizeLimitExceededException caught," + " return truncated results. Original message: "
+ e.getMessage() + " explanation: " + e.getExplanation());
list.setTotalSize(-2);
} finally {
results.close();
}
log.debug("LDAP search returned " + list.size() + " results");
return list;
}
protected DocumentModel ldapResultToDocumentModel(SearchResult result, String entryId, boolean fetchReferences)
throws DirectoryException, NamingException {
Attributes attributes = result.getAttributes();
String passwordFieldId = getPasswordField();
Map<String, Object> fieldMap = new HashMap<String, Object>();
Attribute attribute = attributes.get(idAttribute);
// NXP-2461: check that id field is filled + NXP-2730: make sure that
// entry id is the one returned from LDAP
if (attribute != null) {
Object entry = attribute.get();
if (entry != null) {
entryId = entry.toString();
}
}
// NXP-7136 handle id case
entryId = changeEntryIdCase(entryId, idCase);
if (entryId == null) {
// don't bother
return null;
}
for (String fieldName : schemaFieldMap.keySet()) {
List<Reference> references = directory.getReferences(fieldName);
if (references != null && references.size() > 0) {
if (fetchReferences) {
Map<String, List<String>> referencedIdsMap = new HashMap<>();
for (Reference reference : references) {
// reference resolution
List<String> referencedIds;
if (reference instanceof LDAPReference) {
// optim: use the current LDAPSession directly to
// provide the LDAP reference with the needed backend entries
LDAPReference ldapReference = (LDAPReference) reference;
referencedIds = ldapReference.getLdapTargetIds(attributes);
} else if (reference instanceof LDAPTreeReference) {
// TODO: optimize using the current LDAPSession
// directly to provide the LDAP reference with the
// needed backend entries (needs to implement getLdapTargetIds)
LDAPTreeReference ldapReference = (LDAPTreeReference) reference;
referencedIds = ldapReference.getTargetIdsForSource(entryId);
} else {
referencedIds = reference.getTargetIdsForSource(entryId);
}
referencedIds = new ArrayList<>(referencedIds);
Collections.sort(referencedIds);
if (referencedIdsMap.containsKey(fieldName)) {
referencedIdsMap.get(fieldName).addAll(referencedIds);
} else {
referencedIdsMap.put(fieldName, referencedIds);
}
}
fieldMap.put(fieldName, referencedIdsMap.get(fieldName));
}
} else {
// manage directly stored fields
String attributeId = getDirectory().getFieldMapper().getBackendField(fieldName);
if (attributeId.equals(LDAPDirectory.DN_SPECIAL_ATTRIBUTE_KEY)) {
// this is the special DN readonly attribute
try {
fieldMap.put(fieldName, result.getNameInNamespace());
} catch (UnsupportedOperationException e) {
// ignore ApacheDS partial implementation when running
// in embedded mode
}
} else {
// this is a regular attribute
attribute = attributes.get(attributeId);
if (fieldName.equals(passwordFieldId)) {
// do not try to fetch the password attribute
continue;
} else {
fieldMap.put(fieldName, getFieldValue(attribute, fieldName, entryId, fetchReferences));
}
}
}
}
// check if the idAttribute was returned from the search. If not
// set it anyway, maybe changing its case if it's a String instance
String fieldId = getDirectory().getFieldMapper().getDirectoryField(idAttribute);
Object obj = fieldMap.get(fieldId);
if (obj == null) {
fieldMap.put(fieldId, changeEntryIdCase(entryId, getDirectory().getDescriptor().getMissingIdFieldCase()));
} else if (obj instanceof String) {
fieldMap.put(fieldId, changeEntryIdCase((String) obj, idCase));
}
return fieldMapToDocumentModel(fieldMap);
}
protected String changeEntryIdCase(String id, String idFieldCase) {
if (MISSING_ID_LOWER_CASE.equals(idFieldCase)) {
return id.toLowerCase();
} else if (MISSING_ID_UPPER_CASE.equals(idFieldCase)) {
return id.toUpperCase();
}
// returns the unchanged id
return id;
}
@Override
public boolean authenticate(String username, String password) throws DirectoryException {
if (password == null || "".equals(password.trim())) {
// never use anonymous bind as a way to authenticate a user in
// Nuxeo EP
return false;
}
// lookup the user: fetch its dn
SearchResult entry;
try {
entry = getLdapEntry(username);
} catch (NamingException e) {
throw new DirectoryException("failed to fetch the ldap entry for " + username, e);
}
if (entry == null) {
// no such user => authentication failed
return false;
}
String dn = entry.getNameInNamespace();
Properties env = (Properties) getDirectory().getContextProperties().clone();
env.put(Context.SECURITY_PRINCIPAL, dn);
env.put(Context.SECURITY_CREDENTIALS, password);
InitialLdapContext authenticationDirContext = null;
try {
// creating a context does a bind
log.debug(String.format("LDAP bind dn='%s'", dn));
// noinspection ResultOfObjectAllocationIgnored
authenticationDirContext = new InitialLdapContext(env, null);
// force reconnection to prevent from using a previous connection
// with an obsolete password (after an user has changed his
// password)
authenticationDirContext.reconnect(null);
log.debug("Bind succeeded, authentication ok");
return true;
} catch (NamingException e) {
log.debug("Bind failed: " + e.getMessage());
// authentication failed
return false;
} finally {
try {
if (authenticationDirContext != null) {
authenticationDirContext.close();
}
} catch (NamingException e) {
log.error("Error closing authentication context when biding dn " + dn, e);
return false;
}
}
}
@Override
public boolean isAuthenticating() throws DirectoryException {
String password = getPasswordField();
return schemaFieldMap.containsKey(password);
}
public boolean rdnMatchesIdField() {
return getDirectory().getDescriptor().rdnAttribute.equals(idAttribute);
}
@SuppressWarnings("unchecked")
protected List<String> getMandatoryAttributes(Attribute objectClassesAttribute) throws DirectoryException {
try {
List<String> mandatoryAttributes = new ArrayList<String>();
DirContext schema = dirContext.getSchema("");
List<String> objectClasses = new ArrayList<String>();
if (objectClassesAttribute == null) {
// use the creation classes as reference schema for this entry
objectClasses.addAll(Arrays.asList(getDirectory().getDescriptor().getCreationClasses()));
} else {
// introspec the objectClass definitions to find the mandatory
// attributes for this entry
NamingEnumeration<Object> values = null;
try {
values = (NamingEnumeration<Object>) objectClassesAttribute.getAll();
while (values.hasMore()) {
objectClasses.add(values.next().toString().trim());
}
} catch (NamingException e) {
throw new DirectoryException(e);
} finally {
if (values != null) {
values.close();
}
}
}
objectClasses.remove("top");
for (String creationClass : objectClasses) {
Attributes attributes = schema.getAttributes("ClassDefinition/" + creationClass);
Attribute attribute = attributes.get("MUST");
if (attribute != null) {
NamingEnumeration<String> values = (NamingEnumeration<String>) attribute.getAll();
try {
while (values.hasMore()) {
String value = values.next();
mandatoryAttributes.add(value);
}
} finally {
values.close();
}
}
}
return mandatoryAttributes;
} catch (NamingException e) {
throw new DirectoryException("getMandatoryAttributes failed", e);
}
}
protected List<String> getMandatoryAttributes() throws DirectoryException {
return getMandatoryAttributes(null);
}
@Override
// useful for the log function
public String toString() {
return String.format("LDAPSession '%s' for directory %s", sid, directory.getName());
}
@Override
public DocumentModel createEntry(DocumentModel entry) {
Map<String, Object> fieldMap = entry.getProperties(directory.getSchema());
Map<String, Object> simpleNameFieldMap = new HashMap<String, Object>();
for (Map.Entry<String, Object> fieldEntry : fieldMap.entrySet()) {
String fieldKey = fieldEntry.getKey();
if (fieldKey.contains(":")) {
fieldKey = fieldKey.split(":")[1];
}
simpleNameFieldMap.put(fieldKey, fieldEntry.getValue());
}
return createEntry(simpleNameFieldMap);
}
}