UserSearchEngine.java 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. /*
  2. * Password Management Servlets (PWM)
  3. * http://www.pwm-project.org
  4. *
  5. * Copyright (c) 2006-2009 Novell, Inc.
  6. * Copyright (c) 2009-2020 The PWM Project
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. package password.pwm.ldap.search;
  21. import com.novell.ldapchai.ChaiUser;
  22. import com.novell.ldapchai.exception.ChaiOperationException;
  23. import com.novell.ldapchai.exception.ChaiUnavailableException;
  24. import com.novell.ldapchai.provider.ChaiProvider;
  25. import password.pwm.AppProperty;
  26. import password.pwm.PwmApplication;
  27. import password.pwm.PwmConstants;
  28. import password.pwm.bean.SessionLabel;
  29. import password.pwm.bean.UserIdentity;
  30. import password.pwm.config.Configuration;
  31. import password.pwm.config.PwmSetting;
  32. import password.pwm.config.option.DuplicateMode;
  33. import password.pwm.config.profile.LdapProfile;
  34. import password.pwm.config.value.data.FormConfiguration;
  35. import password.pwm.error.ErrorInformation;
  36. import password.pwm.error.PwmError;
  37. import password.pwm.error.PwmException;
  38. import password.pwm.error.PwmOperationalException;
  39. import password.pwm.error.PwmUnrecoverableException;
  40. import password.pwm.health.HealthRecord;
  41. import password.pwm.svc.PwmService;
  42. import password.pwm.util.PwmScheduler;
  43. import password.pwm.util.java.AtomicLoopIntIncrementer;
  44. import password.pwm.util.java.ConditionalTaskExecutor;
  45. import password.pwm.util.java.JavaHelper;
  46. import password.pwm.util.java.JsonUtil;
  47. import password.pwm.util.java.StatisticIntCounterMap;
  48. import password.pwm.util.java.StringUtil;
  49. import password.pwm.util.java.TimeDuration;
  50. import password.pwm.util.logging.PwmLogLevel;
  51. import password.pwm.util.logging.PwmLogger;
  52. import java.time.Instant;
  53. import java.util.ArrayList;
  54. import java.util.Collection;
  55. import java.util.Collections;
  56. import java.util.HashSet;
  57. import java.util.Iterator;
  58. import java.util.LinkedHashMap;
  59. import java.util.List;
  60. import java.util.Locale;
  61. import java.util.Map;
  62. import java.util.Objects;
  63. import java.util.Set;
  64. import java.util.TreeMap;
  65. import java.util.concurrent.ArrayBlockingQueue;
  66. import java.util.concurrent.ExecutionException;
  67. import java.util.concurrent.FutureTask;
  68. import java.util.concurrent.RejectedExecutionException;
  69. import java.util.concurrent.ThreadFactory;
  70. import java.util.concurrent.ThreadPoolExecutor;
  71. import java.util.concurrent.TimeUnit;
  72. public class UserSearchEngine implements PwmService
  73. {
  74. private static final PwmLogger LOGGER = PwmLogger.forClass( UserSearchEngine.class );
  75. private final StatisticIntCounterMap<SearchStatistic> counters = new StatisticIntCounterMap<>( SearchStatistic.class );
  76. private final AtomicLoopIntIncrementer searchIdCounter = new AtomicLoopIntIncrementer();
  77. enum SearchStatistic
  78. {
  79. searchCounter,
  80. foregroundJobCounter,
  81. backgroundJobCounter,
  82. backgroundRejectionJobCounter,
  83. backgroundCanceledJobCounter,
  84. backgroundJobTimeoutCounter,
  85. }
  86. private PwmApplication pwmApplication;
  87. private ThreadPoolExecutor executor;
  88. private final ConditionalTaskExecutor debugOutputTask = new ConditionalTaskExecutor(
  89. this::periodicDebugOutput,
  90. new ConditionalTaskExecutor.TimeDurationPredicate( 1, TimeDuration.Unit.MINUTES )
  91. );
  92. public UserSearchEngine( )
  93. {
  94. }
  95. @Override
  96. public STATUS status( )
  97. {
  98. return STATUS.OPEN;
  99. }
  100. @Override
  101. public void init( final PwmApplication pwmApplication ) throws PwmException
  102. {
  103. this.pwmApplication = pwmApplication;
  104. this.executor = createExecutor( pwmApplication );
  105. this.periodicDebugOutput();
  106. }
  107. @Override
  108. public void close( )
  109. {
  110. if ( executor != null )
  111. {
  112. executor.shutdown();
  113. }
  114. executor = null;
  115. }
  116. @Override
  117. public List<HealthRecord> healthCheck( )
  118. {
  119. return Collections.emptyList();
  120. }
  121. @Override
  122. public ServiceInfoBean serviceInfo( )
  123. {
  124. return ServiceInfoBean.builder().debugProperties( debugProperties() ).build();
  125. }
  126. public UserIdentity resolveUsername(
  127. final String username,
  128. final String context,
  129. final String profile,
  130. final SessionLabel sessionLabel
  131. )
  132. throws PwmUnrecoverableException, PwmOperationalException
  133. {
  134. //check if username is a key
  135. {
  136. UserIdentity inputIdentity = null;
  137. try
  138. {
  139. inputIdentity = UserIdentity.fromKey( username, pwmApplication );
  140. }
  141. catch ( final PwmException e )
  142. {
  143. /* input is not a userIdentity */
  144. }
  145. if ( inputIdentity != null )
  146. {
  147. try
  148. {
  149. final ChaiUser theUser = pwmApplication.getProxiedChaiUser( inputIdentity );
  150. if ( theUser.exists() )
  151. {
  152. final String canonicalDN;
  153. canonicalDN = theUser.readCanonicalDN();
  154. return new UserIdentity( canonicalDN, inputIdentity.getLdapProfileID() );
  155. }
  156. }
  157. catch ( final ChaiOperationException e )
  158. {
  159. throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER, e.getMessage() ) );
  160. }
  161. catch ( final ChaiUnavailableException e )
  162. {
  163. throw PwmUnrecoverableException.fromChaiException( e );
  164. }
  165. }
  166. }
  167. try
  168. {
  169. //see if we need to do a contextless search.
  170. if ( checkIfStringIsDN( username, sessionLabel ) )
  171. {
  172. return resolveUserDN( username, sessionLabel );
  173. }
  174. else
  175. {
  176. final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
  177. builder.username( username );
  178. if ( context != null )
  179. {
  180. builder.contexts( Collections.singletonList( context ) );
  181. }
  182. if ( profile != null )
  183. {
  184. builder.ldapProfile( profile );
  185. }
  186. final SearchConfiguration searchConfiguration = builder.build();
  187. return performSingleUserSearch( searchConfiguration, sessionLabel );
  188. }
  189. }
  190. catch ( final PwmOperationalException e )
  191. {
  192. throw new PwmOperationalException( new ErrorInformation(
  193. PwmError.ERROR_CANT_MATCH_USER,
  194. e.getErrorInformation().getDetailedErrorMsg(),
  195. e.getErrorInformation().getFieldValues() )
  196. );
  197. }
  198. }
  199. public UserIdentity performSingleUserSearch(
  200. final SearchConfiguration searchConfiguration,
  201. final SessionLabel sessionLabel
  202. )
  203. throws PwmUnrecoverableException, PwmOperationalException
  204. {
  205. final Instant startTime = Instant.now();
  206. final DuplicateMode dupeMode = pwmApplication.getConfig().readSettingAsEnum( PwmSetting.LDAP_DUPLICATE_MODE, DuplicateMode.class );
  207. final int searchCount = ( dupeMode == DuplicateMode.FIRST_ALL ) ? 1 : 2;
  208. final Map<UserIdentity, Map<String, String>> searchResults = performMultiUserSearch( searchConfiguration, searchCount, Collections.emptyList(), sessionLabel );
  209. final List<UserIdentity> results = searchResults == null ? Collections.emptyList() : new ArrayList<>( searchResults.keySet() );
  210. if ( results.isEmpty() )
  211. {
  212. final String errorMessage;
  213. if ( searchConfiguration.getUsername() != null && searchConfiguration.getUsername().length() > 0 )
  214. {
  215. errorMessage = "an ldap user for username value '" + searchConfiguration.getUsername() + "' was not found";
  216. }
  217. else
  218. {
  219. errorMessage = "an ldap user was not found";
  220. }
  221. throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER, errorMessage ) );
  222. }
  223. else if ( results.size() == 1 )
  224. {
  225. final String userDN = results.get( 0 ).getUserDN();
  226. LOGGER.debug( sessionLabel, () -> "found userDN: " + userDN + " (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
  227. return results.get( 0 );
  228. }
  229. if ( dupeMode == DuplicateMode.FIRST_PROFILE )
  230. {
  231. final String profile1 = results.get( 0 ).getLdapProfileID();
  232. final String profile2 = results.get( 1 ).getLdapProfileID();
  233. final boolean sameProfile = ( profile1 == null && profile2 == null )
  234. || ( profile1 != null && profile1.equals( profile2 ) );
  235. if ( sameProfile )
  236. {
  237. final String errorMessage = "multiple user matches in single profile";
  238. throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER, errorMessage ) );
  239. }
  240. LOGGER.trace( sessionLabel, () -> "found multiple matches, but will use first match since second match"
  241. + " is in a different profile and dupeMode is set to "
  242. + DuplicateMode.FIRST_PROFILE );
  243. return results.get( 0 );
  244. }
  245. final String errorMessage = "multiple user matches found";
  246. throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER, errorMessage ) );
  247. }
  248. public UserSearchResults performMultiUserSearchFromForm(
  249. final Locale locale,
  250. final SearchConfiguration searchConfiguration,
  251. final int maxResults,
  252. final List<FormConfiguration> formItem,
  253. final SessionLabel sessionLabel
  254. )
  255. throws PwmUnrecoverableException, PwmOperationalException
  256. {
  257. final Map<String, String> attributeHeaderMap = UserSearchResults.fromFormConfiguration( formItem, locale );
  258. final Map<UserIdentity, Map<String, String>> searchResults = performMultiUserSearch(
  259. searchConfiguration,
  260. maxResults + 1,
  261. attributeHeaderMap.keySet(),
  262. sessionLabel
  263. );
  264. final boolean resultsExceeded = searchResults.size() > maxResults;
  265. final Map<UserIdentity, Map<String, String>> returnData = new LinkedHashMap<>();
  266. for ( final Map.Entry<UserIdentity, Map<String, String>> entry : searchResults.entrySet() )
  267. {
  268. final UserIdentity loopUser = entry.getKey();
  269. returnData.put( loopUser, entry.getValue() );
  270. if ( returnData.size() >= maxResults )
  271. {
  272. break;
  273. }
  274. }
  275. return new UserSearchResults( attributeHeaderMap, returnData, resultsExceeded );
  276. }
  277. public Map<UserIdentity, Map<String, String>> performMultiUserSearch(
  278. final SearchConfiguration searchConfiguration,
  279. final int maxResults,
  280. final Collection<String> returnAttributes,
  281. final SessionLabel sessionLabel
  282. )
  283. throws PwmUnrecoverableException, PwmOperationalException
  284. {
  285. final Collection<LdapProfile> ldapProfiles;
  286. if ( searchConfiguration.getLdapProfile() != null && !searchConfiguration.getLdapProfile().isEmpty() )
  287. {
  288. if ( pwmApplication.getConfig().getLdapProfiles().containsKey( searchConfiguration.getLdapProfile() ) )
  289. {
  290. ldapProfiles = Collections.singletonList( pwmApplication.getConfig().getLdapProfiles().get( searchConfiguration.getLdapProfile() ) );
  291. }
  292. else
  293. {
  294. LOGGER.debug( sessionLabel, () -> "attempt to search for users in unknown ldap profile '"
  295. + searchConfiguration.getLdapProfile() + "', skipping search" );
  296. return Collections.emptyMap();
  297. }
  298. }
  299. else
  300. {
  301. ldapProfiles = pwmApplication.getConfig().getLdapProfiles().values();
  302. }
  303. final boolean ignoreUnreachableProfiles = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.LDAP_IGNORE_UNREACHABLE_PROFILES );
  304. final List<String> errors = new ArrayList<>();
  305. counters.increment( SearchStatistic.searchCounter );
  306. final int searchID = searchIdCounter.next();
  307. final long profileRetryDelayMS = Long.parseLong( pwmApplication.getConfig().readAppProperty( AppProperty.LDAP_PROFILE_RETRY_DELAY ) );
  308. final AtomicLoopIntIncrementer jobIncrementer = AtomicLoopIntIncrementer.builder().build();
  309. final List<UserSearchJob> searchJobs = new ArrayList<>();
  310. for ( final LdapProfile ldapProfile : ldapProfiles )
  311. {
  312. boolean skipProfile = false;
  313. final Instant lastLdapFailure = pwmApplication.getLdapConnectionService().getLastLdapFailureTime( ldapProfile );
  314. if ( ldapProfiles.size() > 1 && lastLdapFailure != null && TimeDuration.fromCurrent( lastLdapFailure ).isShorterThan( profileRetryDelayMS ) )
  315. {
  316. LOGGER.info( () -> "skipping user search on ldap profile " + ldapProfile.getIdentifier() + " due to recent unreachable status ("
  317. + TimeDuration.fromCurrent( lastLdapFailure ).asCompactString() + ")" );
  318. skipProfile = true;
  319. }
  320. if ( !skipProfile )
  321. {
  322. try
  323. {
  324. searchJobs.addAll( this.makeSearchJobs(
  325. ldapProfile,
  326. searchConfiguration,
  327. maxResults,
  328. returnAttributes,
  329. sessionLabel,
  330. searchID,
  331. jobIncrementer
  332. ) );
  333. }
  334. catch ( final PwmUnrecoverableException e )
  335. {
  336. if ( e.getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE )
  337. {
  338. pwmApplication.getLdapConnectionService().setLastLdapFailure( ldapProfile, e.getErrorInformation() );
  339. if ( ignoreUnreachableProfiles )
  340. {
  341. errors.add( e.getErrorInformation().getDetailedErrorMsg() );
  342. if ( errors.size() >= ldapProfiles.size() )
  343. {
  344. final String errorMsg = "all ldap profiles are unreachable; errors: " + JsonUtil.serializeCollection( errors );
  345. throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_DIRECTORY_UNAVAILABLE, errorMsg ) );
  346. }
  347. }
  348. }
  349. else
  350. {
  351. throw e;
  352. }
  353. }
  354. }
  355. }
  356. final Map<UserIdentity, Map<String, String>> resultsMap = new LinkedHashMap<>( executeSearchJobs( searchJobs ) );
  357. final Map<UserIdentity, Map<String, String>> returnMap = trimOrderedMap( resultsMap, maxResults );
  358. return Collections.unmodifiableMap( returnMap );
  359. }
  360. private Collection<UserSearchJob> makeSearchJobs(
  361. final LdapProfile ldapProfile,
  362. final SearchConfiguration searchConfiguration,
  363. final int maxResults,
  364. final Collection<String> returnAttributes,
  365. final SessionLabel sessionLabel,
  366. final int searchID,
  367. final AtomicLoopIntIncrementer jobIncrementer
  368. )
  369. throws PwmUnrecoverableException, PwmOperationalException
  370. {
  371. // check the search configuration data params
  372. searchConfiguration.validate();
  373. final String inputSearchFilter = searchConfiguration.getFilter() != null && searchConfiguration.getFilter().length() > 1
  374. ? searchConfiguration.getFilter()
  375. : ldapProfile.readSettingAsString( PwmSetting.LDAP_USERNAME_SEARCH_FILTER );
  376. final String searchFilter = makeSearchFilter( ldapProfile, searchConfiguration, inputSearchFilter );
  377. final List<String> searchContexts;
  378. if ( searchConfiguration.getContexts() != null
  379. && !searchConfiguration.getContexts().isEmpty()
  380. && searchConfiguration.getContexts().iterator().next() != null
  381. && searchConfiguration.getContexts().iterator().next().length() > 0
  382. )
  383. {
  384. searchContexts = searchConfiguration.getContexts();
  385. if ( searchConfiguration.isEnableContextValidation() )
  386. {
  387. for ( final String searchContext : searchContexts )
  388. {
  389. validateSpecifiedContext( ldapProfile, searchContext );
  390. }
  391. }
  392. }
  393. else
  394. {
  395. searchContexts = ldapProfile.getRootContexts( pwmApplication );
  396. }
  397. final long timeLimitMS = searchConfiguration.getSearchTimeout() != null
  398. ? searchConfiguration.getSearchTimeout().asMillis()
  399. : ( ldapProfile.readSettingAsLong( PwmSetting.LDAP_SEARCH_TIMEOUT ) * 1000 );
  400. final ChaiProvider chaiProvider = searchConfiguration.getChaiProvider() == null
  401. ? pwmApplication.getProxyChaiProvider( ldapProfile.getIdentifier() )
  402. : searchConfiguration.getChaiProvider();
  403. final List<UserSearchJob> returnMap = new ArrayList<>();
  404. for ( final String loopContext : searchContexts )
  405. {
  406. final UserSearchJobParameters userSearchJobParameters = UserSearchJobParameters.builder()
  407. .ldapProfile( ldapProfile )
  408. .searchFilter( searchFilter )
  409. .context( loopContext )
  410. .returnAttributes( returnAttributes )
  411. .maxResults( maxResults )
  412. .chaiProvider( chaiProvider )
  413. .timeoutMs( timeLimitMS )
  414. .sessionLabel( sessionLabel )
  415. .searchID( searchID )
  416. .jobId( jobIncrementer.next() )
  417. .searchScope( searchConfiguration.getSearchScope() )
  418. .ignoreOperationalErrors( searchConfiguration.isIgnoreOperationalErrors() )
  419. .build();
  420. final UserSearchJob userSearchJob = new UserSearchJob( pwmApplication, this, userSearchJobParameters );
  421. returnMap.add( userSearchJob );
  422. }
  423. return returnMap;
  424. }
  425. private String makeSearchFilter( final LdapProfile ldapProfile, final SearchConfiguration searchConfiguration, final String inputSearchFilter )
  426. {
  427. final String searchFilter;
  428. if ( searchConfiguration.getUsername() != null )
  429. {
  430. final String inputQuery = searchConfiguration.isEnableValueEscaping()
  431. ? StringUtil.escapeLdapFilter( searchConfiguration.getUsername() )
  432. : searchConfiguration.getUsername();
  433. if ( searchConfiguration.isEnableSplitWhitespace()
  434. && ( searchConfiguration.getUsername().split( "\\s" ).length > 1 ) )
  435. {
  436. // split on all whitespace chars
  437. final StringBuilder multiSearchFilter = new StringBuilder();
  438. multiSearchFilter.append( "(&" );
  439. for ( final String queryPart : searchConfiguration.getUsername().split( " " ) )
  440. {
  441. multiSearchFilter.append( "(" );
  442. multiSearchFilter.append( inputSearchFilter.replace( PwmConstants.VALUE_REPLACEMENT_USERNAME, queryPart ) );
  443. multiSearchFilter.append( ")" );
  444. }
  445. multiSearchFilter.append( ")" );
  446. searchFilter = multiSearchFilter.toString();
  447. }
  448. else
  449. {
  450. searchFilter = inputSearchFilter.replace( PwmConstants.VALUE_REPLACEMENT_USERNAME, inputQuery.trim() );
  451. }
  452. }
  453. else if ( searchConfiguration.getGroupDN() != null )
  454. {
  455. final String groupAttr = ldapProfile.readSettingAsString( PwmSetting.LDAP_USER_GROUP_ATTRIBUTE );
  456. searchFilter = "(" + groupAttr + "=" + searchConfiguration.getGroupDN() + ")";
  457. }
  458. else if ( searchConfiguration.getFormValues() != null )
  459. {
  460. searchFilter = figureSearchFilterForParams( searchConfiguration.getFormValues(), inputSearchFilter, searchConfiguration.isEnableValueEscaping() );
  461. }
  462. else
  463. {
  464. searchFilter = inputSearchFilter;
  465. }
  466. return searchFilter;
  467. }
  468. private void validateSpecifiedContext( final LdapProfile profile, final String context )
  469. throws PwmOperationalException, PwmUnrecoverableException
  470. {
  471. Objects.requireNonNull( profile, "ldapProfile can not be null for ldap search context validation" );
  472. Objects.requireNonNull( context, "context can not be null for ldap search context validation" );
  473. final String canonicalContext = profile.readCanonicalDN( pwmApplication, context );
  474. {
  475. final Map<String, String> selectableContexts = profile.getSelectableContexts( pwmApplication );
  476. if ( !JavaHelper.isEmpty( selectableContexts ) && selectableContexts.containsKey( canonicalContext ) )
  477. {
  478. // config pre-validates selectable contexts so this should be permitted
  479. return;
  480. }
  481. }
  482. {
  483. final List<String> rootContexts = profile.getRootContexts( pwmApplication );
  484. if ( !JavaHelper.isEmpty( rootContexts ) )
  485. {
  486. for ( final String rootContext : rootContexts )
  487. {
  488. if ( canonicalContext.endsWith( rootContext ) )
  489. {
  490. return;
  491. }
  492. }
  493. final String msg = "specified search context '" + canonicalContext + "' is not contained by a configured root context";
  494. throw new PwmUnrecoverableException( PwmError.CONFIG_FORMAT_ERROR, msg );
  495. }
  496. }
  497. final String msg = "specified search context '" + canonicalContext + "', but no selectable contexts or root are configured";
  498. throw new PwmOperationalException( PwmError.ERROR_INTERNAL, msg );
  499. }
  500. private boolean checkIfStringIsDN(
  501. final String input,
  502. final SessionLabel sessionLabel
  503. )
  504. {
  505. if ( StringUtil.isEmpty( input ) )
  506. {
  507. return false;
  508. }
  509. //if supplied user name starts with username attr assume its the full dn and skip the search
  510. final Set<String> namingAttributes = new HashSet<>();
  511. for ( final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values() )
  512. {
  513. final String usernameAttribute = ldapProfile.readSettingAsString( PwmSetting.LDAP_NAMING_ATTRIBUTE );
  514. if ( input.toLowerCase().startsWith( usernameAttribute.toLowerCase() + "=" ) )
  515. {
  516. LOGGER.trace( sessionLabel, () -> "username '" + input
  517. + "' appears to be a DN (starts with configured ldap naming attribute '"
  518. + usernameAttribute + "'), skipping username search" );
  519. return true;
  520. }
  521. namingAttributes.add( usernameAttribute );
  522. }
  523. LOGGER.trace( sessionLabel, () -> "username '" + input + "' does not appear to be a DN (does not start with any of the configured ldap naming attributes '"
  524. + StringUtil.collectionToString( namingAttributes, "," )
  525. + "')" );
  526. return false;
  527. }
  528. private UserIdentity resolveUserDN(
  529. final String userDN,
  530. final SessionLabel sessionLabel
  531. )
  532. throws PwmUnrecoverableException, PwmOperationalException
  533. {
  534. LOGGER.trace( sessionLabel, () -> "finding profile for userDN " + userDN );
  535. final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
  536. .filter( "(objectClass=*)" )
  537. .enableContextValidation( false )
  538. .contexts( Collections.singletonList( userDN ) )
  539. .searchScope( SearchConfiguration.SearchScope.base )
  540. .ignoreOperationalErrors( true )
  541. .build();
  542. final Map<UserIdentity, Map<String, String>> results = performMultiUserSearch(
  543. searchConfiguration,
  544. 1,
  545. Collections.singleton( "objectClass" ),
  546. sessionLabel );
  547. if ( results.size() < 1 )
  548. {
  549. throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER ) );
  550. }
  551. else if ( results.size() > 1 )
  552. {
  553. throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER, "duplicate DN matches discovered" ) );
  554. }
  555. final UserIdentity userIdentity = results.keySet().iterator().next();
  556. validateSpecifiedContext( userIdentity.getLdapProfile( pwmApplication.getConfig() ), userIdentity.getUserDN() );
  557. return userIdentity;
  558. }
  559. private Map<UserIdentity, Map<String, String>> executeSearchJobs(
  560. final Collection<UserSearchJob> userSearchJobs
  561. )
  562. throws PwmUnrecoverableException
  563. {
  564. if ( JavaHelper.isEmpty( userSearchJobs ) )
  565. {
  566. return Collections.emptyMap();
  567. }
  568. debugOutputTask.conditionallyExecuteTask();
  569. final UserSearchJobParameters firstParam = userSearchJobs.iterator().next().getUserSearchJobParameters();
  570. final Instant startTime = Instant.now();
  571. {
  572. final String filterText = ", filter: " + firstParam.getSearchFilter();
  573. final SessionLabel sessionLabel = firstParam.getSessionLabel();
  574. final int searchID = firstParam.getSearchID();
  575. log( PwmLogLevel.DEBUG, sessionLabel, searchID, -1, "beginning user search process with " + userSearchJobs.size() + " search jobs" + filterText );
  576. }
  577. // execute jobs
  578. for ( final Iterator<UserSearchJob> iterator = userSearchJobs.iterator(); iterator.hasNext(); )
  579. {
  580. final UserSearchJob jobInfo = iterator.next();
  581. boolean submittedToExecutor = false;
  582. // use current thread to execute one (the last in the loop) task.
  583. if ( executor != null && iterator.hasNext() )
  584. {
  585. try
  586. {
  587. executor.submit( jobInfo.getFutureTask() );
  588. submittedToExecutor = true;
  589. counters.increment( SearchStatistic.backgroundJobCounter );
  590. }
  591. catch ( final RejectedExecutionException e )
  592. {
  593. // executor is full, so revert to running locally
  594. counters.increment( SearchStatistic.backgroundRejectionJobCounter );
  595. }
  596. }
  597. if ( !submittedToExecutor )
  598. {
  599. try
  600. {
  601. jobInfo.getFutureTask().run();
  602. counters.increment( SearchStatistic.foregroundJobCounter );
  603. }
  604. catch ( final Throwable t )
  605. {
  606. log( PwmLogLevel.ERROR, firstParam.getSessionLabel(), firstParam.getSearchID(), firstParam.getJobId(),
  607. "unexpected error running job in local thread: " + t.getMessage() );
  608. }
  609. }
  610. }
  611. final Map<UserIdentity, Map<String, String>> results = aggregateJobResults( userSearchJobs );
  612. log( PwmLogLevel.DEBUG, firstParam.getSessionLabel(), firstParam.getSearchID(), -1, "completed user search process in "
  613. + TimeDuration.fromCurrent( startTime ).asCompactString()
  614. + ", intermediate result size=" + results.size() );
  615. return Collections.unmodifiableMap( results );
  616. }
  617. private Map<UserIdentity, Map<String, String>> aggregateJobResults(
  618. final Collection<UserSearchJob> userSearchJobs
  619. )
  620. throws PwmUnrecoverableException
  621. {
  622. final Map<UserIdentity, Map<String, String>> results = new LinkedHashMap<>();
  623. for ( final UserSearchJob jobInfo : userSearchJobs )
  624. {
  625. final UserSearchJobParameters params = jobInfo.getUserSearchJobParameters();
  626. if ( results.size() > jobInfo.getUserSearchJobParameters().getMaxResults() )
  627. {
  628. final FutureTask<Map<UserIdentity, Map<String, String>>> futureTask = jobInfo.getFutureTask();
  629. if ( !futureTask.isDone() )
  630. {
  631. counters.increment( SearchStatistic.backgroundCanceledJobCounter );
  632. }
  633. jobInfo.getFutureTask().cancel( false );
  634. }
  635. else
  636. {
  637. try
  638. {
  639. results.putAll( jobInfo.getFutureTask().get( ) );
  640. }
  641. catch ( final InterruptedException e )
  642. {
  643. final String errorMsg = "unexpected interruption during search job execution: " + e.getMessage();
  644. log( PwmLogLevel.WARN, params.getSessionLabel(), params.getSearchID(), params.getJobId(), errorMsg );
  645. LOGGER.error( params.getSessionLabel(), () -> errorMsg, e );
  646. throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
  647. }
  648. catch ( final ExecutionException e )
  649. {
  650. final Throwable t = e.getCause();
  651. final ErrorInformation errorInformation;
  652. final String errorMsg = "unexpected error during ldap search ("
  653. + "profile=" + jobInfo.getUserSearchJobParameters().getLdapProfile().getIdentifier() + ")"
  654. + ", error: " + ( t instanceof PwmException ? t.getMessage() : JavaHelper.readHostileExceptionMessage( t ) );
  655. if ( t instanceof PwmException )
  656. {
  657. errorInformation = new ErrorInformation( ( ( PwmException ) t ).getError(), errorMsg );
  658. }
  659. else
  660. {
  661. errorInformation = new ErrorInformation( PwmError.ERROR_LDAP_DATA_ERROR, errorMsg );
  662. }
  663. log( PwmLogLevel.WARN, params.getSessionLabel(), params.getSearchID(), params.getJobId(), "error during user search: " + errorInformation.toDebugStr() );
  664. throw new PwmUnrecoverableException( errorInformation );
  665. }
  666. }
  667. }
  668. return results;
  669. }
  670. private Map<String, String> debugProperties( )
  671. {
  672. final Map<String, String> properties = new TreeMap<>( counters.debugStats() );
  673. properties.put( "jvmThreadCount", Integer.toString( Thread.activeCount() ) );
  674. if ( executor == null )
  675. {
  676. properties.put( "background-enabled", "false" );
  677. }
  678. else
  679. {
  680. properties.put( "background-enabled", "true" );
  681. properties.put( "background-maxPoolSize", Integer.toString( executor.getMaximumPoolSize() ) );
  682. properties.put( "background-activeCount", Integer.toString( executor.getActiveCount() ) );
  683. properties.put( "background-largestPoolSize", Integer.toString( executor.getLargestPoolSize() ) );
  684. properties.put( "background-poolSize", Integer.toString( executor.getPoolSize() ) );
  685. properties.put( "background-queue-size", Integer.toString( executor.getQueue().size() ) );
  686. }
  687. return Collections.unmodifiableMap( properties );
  688. }
  689. private void periodicDebugOutput( )
  690. {
  691. LOGGER.debug( () -> "periodic debug status: " + StringUtil.mapToString( debugProperties() ) );
  692. }
  693. void log( final PwmLogLevel level, final SessionLabel sessionLabel, final int searchID, final int jobID, final String message )
  694. {
  695. final String idMsg = logIdString( searchID, jobID );
  696. LOGGER.log( level, sessionLabel, () -> idMsg + " " + message );
  697. }
  698. private static String logIdString( final int searchID, final int jobID )
  699. {
  700. String idMsg = "searchID=" + searchID;
  701. if ( jobID >= 0 )
  702. {
  703. idMsg += "-" + jobID;
  704. }
  705. return idMsg;
  706. }
  707. private static ThreadPoolExecutor createExecutor( final PwmApplication pwmApplication )
  708. {
  709. final Configuration configuration = pwmApplication.getConfig();
  710. final boolean enabled = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.LDAP_SEARCH_PARALLEL_ENABLE ) );
  711. if ( !enabled )
  712. {
  713. return null;
  714. }
  715. final int endPoints;
  716. {
  717. int counter = 0;
  718. for ( final LdapProfile ldapProfile : configuration.getLdapProfiles().values() )
  719. {
  720. final List<String> rootContexts = ldapProfile.readSettingAsStringArray( PwmSetting.LDAP_CONTEXTLESS_ROOT );
  721. counter += rootContexts.size();
  722. }
  723. endPoints = counter;
  724. }
  725. if ( endPoints > 1 )
  726. {
  727. final int factor = Integer.parseInt( configuration.readAppProperty( AppProperty.LDAP_SEARCH_PARALLEL_FACTOR ) );
  728. final int maxThreads = Integer.parseInt( configuration.readAppProperty( AppProperty.LDAP_SEARCH_PARALLEL_THREAD_MAX ) );
  729. final int threads = Math.min( maxThreads, ( endPoints ) * factor );
  730. final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, UserSearchEngine.class ), true );
  731. final int minThreads = JavaHelper.rangeCheck( 1, 10, endPoints );
  732. LOGGER.trace( () -> "initialized with threads min=" + minThreads + " max=" + threads );
  733. return new ThreadPoolExecutor(
  734. minThreads,
  735. threads,
  736. 1,
  737. TimeUnit.MINUTES,
  738. new ArrayBlockingQueue<>( threads ),
  739. threadFactory
  740. );
  741. }
  742. return null;
  743. }
  744. private static <K, V> Map<K, V> trimOrderedMap( final Map<K, V> inputMap, final int maxEntries )
  745. {
  746. final Map<K, V> returnMap = new LinkedHashMap<>( inputMap );
  747. if ( returnMap.size() > maxEntries )
  748. {
  749. int counter = 0;
  750. for ( final Iterator<K> iterator = returnMap.keySet().iterator(); iterator.hasNext(); )
  751. {
  752. iterator.next();
  753. counter++;
  754. if ( counter > maxEntries )
  755. {
  756. iterator.remove();
  757. }
  758. }
  759. }
  760. return Collections.unmodifiableMap( returnMap );
  761. }
  762. private static String figureSearchFilterForParams(
  763. final Map<FormConfiguration, String> formValues,
  764. final String searchFilter,
  765. final boolean enableValueEscaping
  766. )
  767. {
  768. String newSearchFilter = searchFilter;
  769. for ( final Map.Entry<FormConfiguration, String> entry : formValues.entrySet() )
  770. {
  771. final FormConfiguration formItem = entry.getKey();
  772. final String attrName = "%" + formItem.getName() + "%";
  773. String value = entry.getValue();
  774. if ( enableValueEscaping )
  775. {
  776. value = StringUtil.escapeLdapFilter( value );
  777. }
  778. if ( !formItem.isRequired() )
  779. {
  780. if ( StringUtil.isEmpty( value ) )
  781. {
  782. value = "*";
  783. }
  784. }
  785. newSearchFilter = newSearchFilter.replace( attrName, value );
  786. }
  787. return newSearchFilter;
  788. }
  789. }