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}