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