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