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