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}