001/* 002 * Copyright 2016 Anyware Services 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 */ 016package org.ametys.core.util.ldap; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Hashtable; 021import java.util.List; 022import java.util.Map; 023import java.util.regex.Pattern; 024 025import javax.naming.Context; 026import javax.naming.NamingEnumeration; 027import javax.naming.NamingException; 028import javax.naming.directory.Attribute; 029import javax.naming.directory.SearchControls; 030import javax.naming.directory.SearchResult; 031import javax.naming.ldap.Control; 032import javax.naming.ldap.InitialLdapContext; 033import javax.naming.ldap.LdapContext; 034import javax.naming.ldap.PagedResultsControl; 035import javax.naming.ldap.PagedResultsResponseControl; 036import javax.naming.ldap.SortControl; 037 038import org.apache.avalon.framework.configuration.Configuration; 039import org.apache.avalon.framework.configuration.ConfigurationException; 040import org.apache.avalon.framework.service.ServiceException; 041import org.apache.avalon.framework.service.ServiceManager; 042import org.apache.avalon.framework.service.Serviceable; 043 044import org.ametys.core.datasource.AbstractDataSourceManager.DataSourceDefinition; 045import org.ametys.core.datasource.LDAPDataSourceManager; 046import org.ametys.core.datasource.UnknownDataSourceException; 047import org.ametys.core.util.CachingComponent; 048import org.ametys.runtime.config.Config; 049 050/** 051 * This abstract class contains all basic for a ldap connection using config parameters 052 * @param <K> the type of keys for caching objects by this component. 053 * @param <V> the type of objects cached by this component. 054 */ 055public abstract class AbstractLDAPConnector<K, V> extends CachingComponent<K, V> implements Serviceable 056{ 057 /** The default LDAP search page size */ 058 protected static final int __DEFAULT_PAGE_SIZE = 500; // default limit for OpenLdap 059 060 // Check filter look 061 private static final Pattern __FILTER = Pattern.compile("\\s*\\(.*\\)\\s*"); 062 063 /** URL connection to the ldap server. */ 064 protected String _ldapUrl; 065 /** Base DN to the ldap server. */ 066 protected String _ldapBaseDN; 067 /** Distinguished name of the admin used for searching. */ 068 protected String _ldapAdminRelativeDN; 069 /** Password associated with the admin. */ 070 protected String _ldapAdminPassword; 071 /** Authentication method used. */ 072 protected String _ldapAuthenticationMethod; 073 /** Use ssl for connecting to ldap server. */ 074 protected boolean _ldapUseSSL; 075 /** Enable following referrals. */ 076 protected boolean _ldapFollowReferrals; 077 /** Alias dereferencing mode. */ 078 protected String _ldapAliasDerefMode; 079 /** True to sort the results on the server side, false to get the results unsorted. */ 080 protected boolean _serverSideSorting; 081 082 /** Indicates if the LDAP server supports paging feature. */ 083 protected boolean _pagingSupported; 084 085 /** The LDAP data source manager */ 086 private LDAPDataSourceManager _ldapDataSourceManager; 087 088 /** 089 * Call this method with the datasource id to initialize this component 090 * @param dataSourceId The id of the datasource 091 * @throws Exception If an error occurs. 092 */ 093 protected void _delayedInitialize(String dataSourceId) throws Exception 094 { 095 DataSourceDefinition ldapDefinition = _ldapDataSourceManager.getDataSourceDefinition(dataSourceId); 096 if (ldapDefinition != null) 097 { 098 Map<String, Object> ldapParameters = ldapDefinition.getParameters(); 099 100 _ldapUrl = (String) ldapParameters.get(LDAPDataSourceManager.PARAM_BASE_URL); 101 _ldapBaseDN = (String) ldapParameters.get(LDAPDataSourceManager.PARAM_BASE_DN); 102 _ldapAdminRelativeDN = (String) ldapParameters.get(LDAPDataSourceManager.PARAM_ADMIN_DN); 103 _ldapAdminPassword = (String) ldapParameters.get(LDAPDataSourceManager.PARAM_ADMIN_PASSWORD); 104 _ldapAuthenticationMethod = (String) ldapParameters.get(LDAPDataSourceManager.PARAM_AUTHENTICATION_METHOD); 105 _ldapUseSSL = (boolean) ldapParameters.get(LDAPDataSourceManager.PARAM_USE_SSL); 106 _ldapFollowReferrals = (boolean) ldapParameters.get(LDAPDataSourceManager.PARAM_FOLLOW_REFERRALS); 107 _ldapAliasDerefMode = (String) ldapParameters.get(LDAPDataSourceManager.PARAM_ALIAS_DEREFERENCING); 108 _serverSideSorting = (boolean) ldapParameters.get(LDAPDataSourceManager.PARAM_SERVER_SIDE_SORTING); 109 110 _pagingSupported = _testPagingSupported(); 111 if (!_testConnectionsPooled()) 112 { 113 getLogger().warn("Warning! LDAP connections for this connector are not pooled. " 114 + "If you are using SSL, you must set the system property 'com.sun.jndi.ldap.connect.pool.protocol' to the String \"plain ssl\"."); 115 } 116 } 117 else 118 { 119 throw new UnknownDataSourceException("The data source of id '" + dataSourceId + "' is still referenced but no longer exists."); 120 } 121 } 122 123 @Override 124 public void service(ServiceManager serviceManager) throws ServiceException 125 { 126 _ldapDataSourceManager = (LDAPDataSourceManager) serviceManager.lookup(LDAPDataSourceManager.ROLE); 127 } 128 129 /** 130 * Get the filter from configuration key and check it 131 * @param configuration The configuration 132 * @param filterKey The name of the child in configuration containing the filter config parameter name 133 * @return The value of the configured filter 134 * @throws ConfigurationException if the filter does not match 135 */ 136 protected String _getFilter(Configuration configuration, String filterKey) throws ConfigurationException 137 { 138 String filter = _getConfigParameter(configuration, filterKey); 139 if (!__FILTER.matcher(filter).matches()) 140 { 141 String message = "Invalid filter '" + filter + "', missing parenthesis"; 142 throw new ConfigurationException(message, configuration); 143 } 144 return filter; 145 } 146 147 /** 148 * Get the search scope from configuration key 149 * @param configuration The configuration 150 * @param searchScopeKey The name of the child in configuration containing the search scope parameter name 151 * @return The scope between <code>SearchControls.ONELEVEL_SCOPE</code>, <code>SearchControls.SUBTREE_SCOPE</code> and <code>SearchControls.OBJECT_SCOPE</code>. 152 * @throws ConfigurationException if a configuration problem occurs 153 */ 154 protected int _getSearchScope(Configuration configuration, String searchScopeKey) throws ConfigurationException 155 { 156 String usersSearchScope = _getConfigParameter(configuration, searchScopeKey); 157 158 try 159 { 160 return ScopeEnumerator.parseScope(usersSearchScope); 161 } 162 catch (IllegalArgumentException e) 163 { 164 throw new ConfigurationException("Unable to parse scope", e); 165 } 166 } 167 168 /** 169 * Test if paging is supported by the underlying directory server. 170 * @return true if the server supports paging. 171 */ 172 public boolean isPagingSupported() 173 { 174 return _pagingSupported; 175 } 176 177 /** 178 * Get a config parameter value 179 * @param configuration The configuration 180 * @param key The child node of configuration containing the config parameter name 181 * @return The value (can be null) 182 * @throws ConfigurationException if parameter is missing 183 */ 184 protected String _getConfigParameter(Configuration configuration, String key) throws ConfigurationException 185 { 186 String parameterName = configuration.getChild(key).getValue(null); 187 if (parameterName == null) 188 { 189 String message = "The parameter '" + key + "' is missing"; 190 getLogger().error(message); 191 throw new ConfigurationException(message, configuration); 192 } 193 194 String valeur = Config.getInstance().getValue(parameterName); 195 return valeur; 196 } 197 198 /** 199 * Get the parameters for connecting to the ldap server. 200 * 201 * @return Parameters for connecting. 202 */ 203 protected Hashtable<String, String> _getContextEnv() 204 { 205 Hashtable<String, String> env = new Hashtable<>(); 206 207 env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 208 env.put(Context.PROVIDER_URL, _ldapUrl + "/" + _ldapBaseDN); 209 env.put(Context.SECURITY_AUTHENTICATION, _ldapAuthenticationMethod); 210 211 if (!_ldapAuthenticationMethod.equals("none")) 212 { 213 env.put(Context.SECURITY_PRINCIPAL, _ldapAdminRelativeDN); 214 env.put(Context.SECURITY_CREDENTIALS, _ldapAdminPassword); 215 } 216 217 if (_ldapUseSSL) 218 { 219 // Encrypt the connection to the server with SSL 220 env.put(Context.SECURITY_PROTOCOL, "ssl"); 221 } 222 223 // Default is to ignore. 224 if (_ldapFollowReferrals) 225 { 226 env.put(Context.REFERRAL, "follow"); 227 } 228 else 229 { 230 env.put(Context.REFERRAL, "ignore"); 231 } 232 233 env.put("java.naming.ldap.derefAliases", _ldapAliasDerefMode); 234 235 // Use ldap pool connection 236 env.put("com.sun.jndi.ldap.connect.pool", "true"); 237 238 return env; 239 } 240 241 /** 242 * Get the parameters for connecting to the ldap server, root DN. 243 * @return Parameters for connecting. 244 */ 245 protected Hashtable<String, String> _getRootContextEnv() 246 { 247 Hashtable<String, String> env = _getContextEnv(); 248 249 env.put(Context.PROVIDER_URL, _ldapUrl); 250 251 return env; 252 } 253 254 /** 255 * Test if paging is supported by the underlying directory server. 256 * @return true if the server supports paging. 257 */ 258 protected boolean _testPagingSupported() 259 { 260 boolean supported = false; 261 262 LdapContext context = null; 263 NamingEnumeration<SearchResult> results = null; 264 265 try 266 { 267 // Connection to LDAP server 268 context = new InitialLdapContext(_getRootContextEnv(), null); 269 270 SearchControls controls = new SearchControls(); 271 controls.setReturningAttributes(new String[]{"supportedControl"}); 272 controls.setSearchScope(SearchControls.OBJECT_SCOPE); 273 274 // Search for the rootDSE object. 275 results = context.search("", "(objectClass=*)", controls); 276 277 while (results.hasMore() && !supported) 278 { 279 SearchResult entry = results.next(); 280 NamingEnumeration<?> attrs = entry.getAttributes().getAll(); 281 while (attrs.hasMore() && !supported) 282 { 283 Attribute attr = (Attribute) attrs.next(); 284 NamingEnumeration<?> vals = attr.getAll(); 285 while (vals.hasMore() && !supported) 286 { 287 String value = (String) vals.next(); 288 if (PagedResultsControl.OID.equals(value)) 289 { 290 supported = true; 291 } 292 } 293 } 294 } 295 } 296 catch (NamingException e) 297 { 298 getLogger().warn("Error while testing the LDAP server for paging feature, assuming false.", e); 299 } 300 finally 301 { 302 // Close resources of connection 303 _cleanup(context, results); 304 } 305 306 return supported; 307 } 308 309 /** 310 * Test if connections are pooled 311 * @return true if connections are pooled 312 */ 313 protected boolean _testConnectionsPooled() 314 { 315 return !_ldapUseSSL || "plain ssl".equals(System.getProperty("com.sun.jndi.ldap.connect.pool.protocol")); 316 } 317 318 /** 319 * Clean a connection to an ldap server. 320 * 321 * @param context The connection to the database to close. 322 * @param result The result to close. 323 */ 324 protected void _cleanup(Context context, NamingEnumeration result) 325 { 326 if (result != null) 327 { 328 try 329 { 330 // Close results 331 result.close(); 332 } 333 catch (NamingException e) 334 { 335 getLogger().error("Error while closing ldap result", e); 336 } 337 } 338 if (context != null) 339 { 340 try 341 { 342 // Close server connection 343 context.close(); 344 } 345 catch (NamingException e) 346 { 347 getLogger().error("Error while closing ldap connection", e); 348 } 349 } 350 } 351 352 /** 353 * Executes a LDAP search 354 * @param pageSize The number of entries in a page 355 * @param name the name of the context or object to search 356 * @param filter the filter expression to use for the search 357 * @param searchControls the search controls that control the search. 358 * @return The results of the LDAP search 359 * @throws NamingException if a naming exception is encountered 360 */ 361 protected List<SearchResult> _search(int pageSize, String name, String filter, SearchControls searchControls) throws NamingException 362 { 363 return _search(pageSize, name, filter, null, searchControls, 0, Integer.MAX_VALUE, false); 364 } 365 366 /** 367 * Executes a LDAP search 368 * @param pageSize The number of entries in a page 369 * @param name the name of the context or object to search 370 * @param filter the filter expression to use for the search 371 * @param searchControls the search controls that control the search. 372 * @param sorted True to sort the results 373 * @return The results of the LDAP search 374 * @throws NamingException if a naming exception is encountered 375 */ 376 protected List<SearchResult> _search(int pageSize, String name, String filter, SearchControls searchControls, boolean sorted) throws NamingException 377 { 378 return _search(pageSize, name, filter, null, searchControls, 0, Integer.MAX_VALUE, sorted); 379 } 380 381 382 /** 383 * Executes a LDAP search 384 * @param pageSize The number of entries in a page 385 * @param name the name of the context or object to search 386 * @param filter the filter expression to use for the search 387 * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null. 388 * @param searchControls the search controls that control the search. 389 * @param offset the start index 390 * @param limit The max number of results 391 * @return The results of the LDAP search 392 * @throws NamingException if a naming exception is encountered 393 */ 394 protected List<SearchResult> _search(int pageSize, String name, String filter, Object[] filterArgs, SearchControls searchControls, int offset, int limit) throws NamingException 395 { 396 return _search(pageSize, name, filter, filterArgs, searchControls, offset, limit, false); 397 } 398 399 /** 400 * Executes a LDAP search 401 * @param pageSize The number of entries in a page 402 * @param name the name of the context or object to search 403 * @param filter the filter expression to use for the search 404 * @param filterArgs the array of arguments to substitute for the variables in filter. Can be null. 405 * @param searchControls the search controls that control the search. 406 * @param offset the start index 407 * @param limit The max number of results 408 * @param sorted True to sort the results 409 * @return The results of the LDAP search 410 * @throws NamingException if a naming exception is encountered 411 */ 412 protected List<SearchResult> _search(int pageSize, String name, String filter, Object[] filterArgs, SearchControls searchControls, int offset, int limit, boolean sorted) throws NamingException 413 { 414 List<SearchResult> allResults = new ArrayList<>(); 415 416 LdapContext context = null; 417 NamingEnumeration<SearchResult> tmpResults = null; 418 419 try 420 { 421 // Connect to the LDAP server. 422 context = new InitialLdapContext(_getContextEnv(), null); 423 424 _setResultsControls(pageSize, context, sorted); 425 426 int index = 0; 427 do 428 { 429 // Perform the search 430 tmpResults = context.search(name, filter, filterArgs, searchControls); 431 432 // Iterate over a batch of search results 433 while (tmpResults != null && tmpResults.hasMoreElements() && index < offset + limit) 434 { 435 SearchResult result = tmpResults.nextElement(); 436 if (index >= offset) 437 { 438 // Retrieve current entry 439 allResults.add(result); 440 } 441 index++; 442 } 443 } 444 while (index < offset + limit && _hasMoreEntries(pageSize, context)); 445 } 446 finally 447 { 448 // Close connection resources 449 _cleanup(context, tmpResults); 450 } 451 452 return allResults; 453 } 454 455 /** 456 * Determines if there are more entries for the LDAP server to return based on server-generated cookie. 457 * @param pageSize The number of entries to be returned per page 458 * @param context The ldap context 459 * @return false if there are no more entries. 460 * @throws NamingException If an error occurred while getting/setting the request controls 461 */ 462 protected boolean _hasMoreEntries(int pageSize, LdapContext context) throws NamingException 463 { 464 byte[] cookie = null; 465 466 // Examine the paged results control response 467 Control[] controls = context.getResponseControls(); 468 if (controls != null) 469 { 470 for (int i = 0; i < controls.length; i++) 471 { 472 if (controls[i] instanceof PagedResultsResponseControl) 473 { 474 PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i]; 475 cookie = prrc.getCookie(); 476 } 477 } 478 } 479 480 // Re-activate paged results 481 if (isPagingSupported()) 482 { 483 try 484 { 485 context.setRequestControls(new Control[]{new PagedResultsControl(pageSize, cookie, Control.NONCRITICAL) }); 486 } 487 catch (IOException ioe) 488 { 489 getLogger().error("Error setting the PagedResultsControl in the LDAP context.", ioe); 490 } 491 } 492 493 return cookie != null; 494 } 495 496 /** 497 * Set paging on ldap if supported, and set the sort 498 * @param pageSize The page size to communicate with ldap 499 * @param context The ldap context 500 * @param sorted True add the sort controls 501 * @throws NamingException if an error occurred 502 */ 503 protected void _setResultsControls(int pageSize, LdapContext context, boolean sorted) throws NamingException 504 { 505 List<Control> controls = new ArrayList<>(); 506 507 if (isPagingSupported()) 508 { 509 try 510 { 511 controls.add(new PagedResultsControl(pageSize, Control.NONCRITICAL)); 512 } 513 catch (IOException ioe) 514 { 515 getLogger().error("Error setting the PagingResultsControl in the LDAP context.", ioe); 516 } 517 } 518 519 String[] sortByFields = getSortByFields(); 520 if (sorted && sortByFields != null && sortByFields.length > 0) 521 { 522 try 523 { 524 controls.add(new SortControl(sortByFields, Control.NONCRITICAL)); 525 } 526 catch (IOException ioe) 527 { 528 getLogger().error("Error setting the SortControl in the LDAP context.", ioe); 529 } 530 } 531 532 if (controls.size() > 0) 533 { 534 context.setRequestControls(controls.toArray(new Control[controls.size()])); 535 } 536 } 537 538 /** 539 * Get the fields to sort by if the search is sorted 540 * @return The list of fields to sort by 541 */ 542 protected String[] getSortByFields() 543 { 544 return null; 545 } 546}