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}