001/*
002 * (C) Copyright 2006-2018 Nuxeo (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 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.directory.ldap;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import javax.naming.directory.SearchControls;
029
030import org.apache.commons.lang3.StringUtils;
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.nuxeo.common.xmap.annotation.XNode;
034import org.nuxeo.common.xmap.annotation.XNodeList;
035import org.nuxeo.common.xmap.annotation.XNodeMap;
036import org.nuxeo.common.xmap.annotation.XObject;
037import org.nuxeo.ecm.directory.BaseDirectoryDescriptor;
038import org.nuxeo.ecm.directory.DirectoryException;
039import org.nuxeo.ecm.directory.EntryAdaptor;
040import org.nuxeo.ecm.directory.Reference;
041
042@XObject(value = "directory")
043public class LDAPDirectoryDescriptor extends BaseDirectoryDescriptor {
044
045    public static final Log log = LogFactory.getLog(LDAPDirectoryDescriptor.class);
046
047    public static final int DEFAULT_SEARCH_SCOPE = SearchControls.ONELEVEL_SCOPE;
048
049    public static final String DEFAULT_SEARCH_CLASSES_FILTER = "(objectClass=*)";
050
051    public static final String DEFAULT_EMPTY_REF_MARKER = "cn=emptyRef";
052
053    public static final String DEFAULT_MISSING_ID_FIELD_CASE = "unchanged";
054
055    public static final String DEFAULT_ID_CASE = "unchanged";
056
057    public static final int DEFAULT_QUERY_SIZE_LIMIT = 200;
058
059    public static final int DEFAULT_QUERY_TIME_LIMIT = 0; // default to wait indefinitely
060
061    public static final boolean DEFAULT_FOLLOW_REFERRALS = true;
062
063    @XNode("server")
064    public String serverName;
065
066    @XNode("searchBaseDn")
067    public String searchBaseDn;
068
069    @XNodeMap(value = "fieldMapping", key = "@name", type = HashMap.class, componentType = String.class)
070    public Map<String, String> fieldMapping = new HashMap<>();
071
072    public String[] searchClasses;
073
074    public String searchClassesFilter;
075
076    public String searchFilter;
077
078    public Integer searchScope;
079
080    @XNode("creationBaseDn")
081    public String creationBaseDn;
082
083    @XNodeList(value = "creationClass", componentType = String.class, type = String[].class)
084    public String[] creationClasses;
085
086    @XNode("rdnAttribute")
087    public String rdnAttribute;
088
089    @XNodeList(value = "references/ldapReference", type = LDAPReference[].class, componentType = LDAPReference.class)
090    private LDAPReference[] ldapReferences;
091
092    @XNodeList(value = "references/ldapTreeReference", type = LDAPTreeReference[].class, componentType = LDAPTreeReference.class)
093    private LDAPTreeReference[] ldapTreeReferences;
094
095    @XNode("emptyRefMarker")
096    public String emptyRefMarker;
097
098    @XNode("missingIdFieldCase")
099    public String missingIdFieldCase;
100
101    public String getMissingIdFieldCase() {
102        return missingIdFieldCase == null ? DEFAULT_MISSING_ID_FIELD_CASE : missingIdFieldCase;
103    }
104
105    /**
106     * Since 5.4.2: force id case to upper or lower, or leaver it unchanged.
107     */
108    @XNode("idCase")
109    public String idCase = DEFAULT_ID_CASE;
110
111    @XNode("querySizeLimit")
112    private Integer querySizeLimit;
113
114    @XNode("queryTimeLimit")
115    private Integer queryTimeLimit;
116
117    // Add attribute to allow to ignore referrals resolution
118    /**
119     * Since 5.9.4
120     */
121    @XNode("followReferrals")
122    protected Boolean followReferrals;
123
124    public boolean getFollowReferrals() {
125        return followReferrals == null ? DEFAULT_FOLLOW_REFERRALS : followReferrals.booleanValue();
126    }
127
128    protected EntryAdaptor entryAdaptor;
129
130    @XObject(value = "entryAdaptor")
131    public static class EntryAdaptorDescriptor {
132
133        @XNode("@class")
134        public Class<? extends EntryAdaptor> adaptorClass;
135
136        @XNodeMap(value = "parameter", key = "@name", type = HashMap.class, componentType = String.class)
137        public Map<String, String> parameters;
138
139    }
140
141    @XNode("entryAdaptor")
142    public void setEntryAdaptor(EntryAdaptorDescriptor adaptorDescriptor)
143            throws InstantiationException, IllegalAccessException {
144        entryAdaptor = adaptorDescriptor.adaptorClass.newInstance();
145        for (Map.Entry<String, String> paramEntry : adaptorDescriptor.parameters.entrySet()) {
146            entryAdaptor.setParameter(paramEntry.getKey(), paramEntry.getValue());
147        }
148    }
149
150    /**
151     * @since 5.7 : allow to contribute custom Exception Handler to extract LDAP validation error messages
152     */
153    @XNode("ldapExceptionHandler")
154    protected Class<? extends LdapExceptionProcessor> exceptionProcessorClass;
155
156    protected LdapExceptionProcessor exceptionProcessor;
157
158    // XXX: passwordEncryption?
159    // XXX: ignoredFields?
160    // XXX: referenceFields?
161    public LDAPDirectoryDescriptor() {
162    }
163
164    public String getRdnAttribute() {
165        return rdnAttribute;
166    }
167
168    public String getCreationBaseDn() {
169        return creationBaseDn;
170    }
171
172    public String[] getCreationClasses() {
173        return creationClasses;
174    }
175
176    public String getIdCase() {
177        return idCase;
178    }
179
180    public String getSearchBaseDn() {
181        return searchBaseDn;
182    }
183
184    @XNodeList(value = "searchClass", componentType = String.class, type = String[].class)
185    public void setSearchClasses(String[] searchClasses) {
186        this.searchClasses = searchClasses;
187        if (searchClasses == null) {
188            // default searchClassesFilter
189            searchClassesFilter = DEFAULT_SEARCH_CLASSES_FILTER;
190            return;
191        }
192        List<String> searchClassFilters = new ArrayList<>();
193        for (String searchClass : searchClasses) {
194            searchClassFilters.add("(objectClass=" + searchClass + ')');
195        }
196        searchClassesFilter = StringUtils.join(searchClassFilters.toArray());
197
198        // logical OR if several classes are provided
199        if (searchClasses.length > 1) {
200            searchClassesFilter = "(|" + searchClassesFilter + ')';
201        }
202    }
203
204    public String[] getSearchClasses() {
205        return searchClasses;
206    }
207
208    @XNode("searchFilter")
209    public void setSearchFilter(String searchFilter) {
210        if ((searchFilter == null) || searchFilter.equals("(objectClass=*)")) {
211            this.searchFilter = null;
212            return;
213        }
214        if (!searchFilter.startsWith("(") && !searchFilter.endsWith(")")) {
215            searchFilter = '(' + searchFilter + ')';
216        }
217        this.searchFilter = searchFilter;
218    }
219
220    public String getSearchFilter() {
221        return searchFilter;
222    }
223
224    @XNode("searchScope")
225    public void setSearchScope(String searchScope) throws DirectoryException {
226        if (searchScope == null) {
227            // restore default search scope
228            this.searchScope = null;
229            return;
230        }
231        Integer scope = LdapScope.getIntegerScope(searchScope);
232        if (scope == null) {
233            // invalid scope
234            throw new DirectoryException(
235                    "Invalid search scope: " + searchScope + ". Valid options: object, onelevel, subtree");
236        }
237        this.searchScope = scope;
238    }
239
240    public int getSearchScope() {
241        return searchScope == null ? DEFAULT_SEARCH_SCOPE : searchScope.intValue();
242    }
243
244    public String getServerName() {
245        return serverName;
246    }
247
248    public String getAggregatedSearchFilter() {
249        if (searchFilter == null) {
250            return searchClassesFilter;
251        }
252        return "(&" + searchClassesFilter + searchFilter + ')';
253    }
254
255    public Map<String, String> getFieldMapping() {
256        return fieldMapping;
257    }
258
259    public void setFieldMapping(Map<String, String> fieldMapping) {
260        this.fieldMapping = fieldMapping;
261    }
262
263    public Reference[] getLdapReferences() {
264        List<Reference> refs = new ArrayList<>();
265        if (ldapReferences != null) {
266            refs.addAll(Arrays.asList(ldapReferences));
267        }
268        if (ldapTreeReferences != null) {
269            refs.addAll(Arrays.asList(ldapTreeReferences));
270        }
271        return refs.toArray(new Reference[] {});
272    }
273
274    public String getEmptyRefMarker() {
275        return emptyRefMarker == null ? DEFAULT_EMPTY_REF_MARKER : emptyRefMarker;
276    }
277
278    public void setEmptyRefMarker(String emptyRefMarker) {
279        this.emptyRefMarker = emptyRefMarker;
280    }
281
282    public int getQuerySizeLimit() {
283        return querySizeLimit == null ? DEFAULT_QUERY_SIZE_LIMIT : querySizeLimit.intValue();
284    }
285
286    public void setQuerySizeLimit(int querySizeLimit) {
287        this.querySizeLimit = Integer.valueOf(querySizeLimit);
288    }
289
290    public void setQueryTimeLimit(int queryTimeLimit) {
291        this.queryTimeLimit = Integer.valueOf(queryTimeLimit);
292    }
293
294    public int getQueryTimeLimit() {
295        return queryTimeLimit == null ? DEFAULT_QUERY_TIME_LIMIT : queryTimeLimit.intValue();
296    }
297
298    public EntryAdaptor getEntryAdaptor() {
299        return entryAdaptor;
300    }
301
302    public LdapExceptionProcessor getExceptionProcessor() {
303        if (exceptionProcessor == null) {
304            if (exceptionProcessorClass == null) {
305                exceptionProcessor = new DefaultLdapExceptionProcessor();
306            } else {
307                try {
308                    exceptionProcessor = exceptionProcessorClass.newInstance();
309                } catch (ReflectiveOperationException e) {
310                    log.error("Unable to instanciate custom Exception handler", e);
311                    exceptionProcessor = new DefaultLdapExceptionProcessor();
312                }
313            }
314        }
315        return exceptionProcessor;
316    }
317
318    @Override
319    public void merge(BaseDirectoryDescriptor other) {
320        super.merge(other);
321        if (other instanceof LDAPDirectoryDescriptor) {
322            merge((LDAPDirectoryDescriptor) other);
323        }
324    }
325
326    protected void merge(LDAPDirectoryDescriptor other) {
327        if (other.serverName != null) {
328            serverName = other.serverName;
329        }
330        if (other.searchBaseDn != null) {
331            searchBaseDn = other.searchBaseDn;
332        }
333        if (other.fieldMapping != null) {
334            fieldMapping.putAll(other.fieldMapping);
335        }
336        if (other.searchClasses != null && other.searchClasses.length > 0) {
337            searchClasses = other.searchClasses.clone();
338        }
339        if (other.searchClassesFilter != null) {
340            searchClassesFilter = other.searchClassesFilter;
341        }
342        if (other.searchFilter != null) {
343            searchFilter = other.searchFilter;
344        }
345        if (other.searchScope != null) {
346            searchScope = other.searchScope;
347        }
348        if (other.creationBaseDn != null) {
349            creationBaseDn = other.creationBaseDn;
350        }
351        if (other.creationClasses != null && other.creationClasses.length > 0) {
352            creationClasses = other.creationClasses.clone();
353        }
354        if (other.rdnAttribute != null) {
355            rdnAttribute = other.rdnAttribute;
356        }
357        if (other.ldapReferences != null && other.ldapReferences.length > 0) {
358            ldapReferences = other.ldapReferences;
359        }
360        if (other.ldapTreeReferences != null && other.ldapTreeReferences.length > 0) {
361            ldapTreeReferences = other.ldapTreeReferences;
362        }
363        if (other.emptyRefMarker != null) {
364            emptyRefMarker = other.emptyRefMarker;
365        }
366        if (other.missingIdFieldCase != null) {
367            missingIdFieldCase = other.missingIdFieldCase;
368        }
369        if (other.idCase != null) {
370            idCase = other.idCase;
371        }
372        if (other.querySizeLimit != null) {
373            querySizeLimit = other.querySizeLimit;
374        }
375        if (other.queryTimeLimit != null) {
376            queryTimeLimit = other.queryTimeLimit;
377        }
378        if (other.followReferrals != null) {
379            followReferrals = other.followReferrals;
380        }
381        if (other.entryAdaptor != null) {
382            entryAdaptor = other.entryAdaptor;
383        }
384        if (other.exceptionProcessorClass != null) {
385            exceptionProcessorClass = other.exceptionProcessorClass;
386            exceptionProcessor = other.exceptionProcessor;
387        }
388    }
389
390    @Override
391    public LDAPDirectoryDescriptor clone() {
392        LDAPDirectoryDescriptor clone = (LDAPDirectoryDescriptor) super.clone();
393        // basic fields are already copied by super.clone()
394        if (fieldMapping != null) {
395            clone.fieldMapping = new HashMap<>(fieldMapping);
396        }
397        if (searchClasses != null) {
398            clone.searchClasses = searchClasses.clone();
399        }
400        if (creationClasses != null) {
401            creationClasses = creationClasses.clone();
402        }
403        if (ldapReferences != null) {
404            clone.ldapReferences = new LDAPReference[ldapReferences.length];
405            for (int i = 0; i < ldapReferences.length; i++) {
406                clone.ldapReferences[i] = ldapReferences[i].clone();
407            }
408        }
409        if (ldapTreeReferences != null) {
410            clone.ldapTreeReferences = new LDAPTreeReference[ldapTreeReferences.length];
411            for (int i = 0; i < ldapTreeReferences.length; i++) {
412                clone.ldapTreeReferences[i] = ldapTreeReferences[i].clone();
413            }
414        }
415        return clone;
416    }
417
418    @Override
419    public LDAPDirectory newDirectory() {
420        return new LDAPDirectory(this);
421    }
422
423}