/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.ldap.search; import com.novell.ldapchai.ChaiFactory; import com.novell.ldapchai.ChaiUser; import com.novell.ldapchai.exception.ChaiOperationException; import com.novell.ldapchai.exception.ChaiUnavailableException; import com.novell.ldapchai.provider.ChaiProvider; import com.novell.ldapchai.util.SearchHelper; import lombok.AllArgsConstructor; import lombok.Getter; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.SessionLabel; import password.pwm.bean.UserIdentity; import password.pwm.config.Configuration; import password.pwm.config.FormConfiguration; import password.pwm.config.PwmSetting; import password.pwm.config.option.DuplicateMode; import password.pwm.config.profile.LdapProfile; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmException; import password.pwm.error.PwmOperationalException; import password.pwm.error.PwmUnrecoverableException; import password.pwm.health.HealthRecord; import password.pwm.svc.PwmService; import password.pwm.svc.stats.Statistic; import password.pwm.util.java.ConditionalTaskExecutor; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.JsonUtil; import password.pwm.util.java.StringUtil; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogLevel; import password.pwm.util.logging.PwmLogger; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; public class UserSearchEngine implements PwmService { private static final PwmLogger LOGGER = PwmLogger.forClass(UserSearchEngine.class); private final AtomicInteger searchCounter = new AtomicInteger(0); private final AtomicInteger foregroundJobCounter = new AtomicInteger(0); private final AtomicInteger backgroundJobCounter = new AtomicInteger(0); private final AtomicInteger rejectionJobCounter = new AtomicInteger(0); private final AtomicInteger canceledJobCounter = new AtomicInteger(0); private final AtomicInteger jobTimeoutCounter = new AtomicInteger(0); private PwmApplication pwmApplication; private ThreadPoolExecutor executor; private final ConditionalTaskExecutor debugOutputTask = new ConditionalTaskExecutor( () -> periodicDebugOutput(), new ConditionalTaskExecutor.TimeDurationPredicate(1, TimeUnit.MINUTES) ); public UserSearchEngine() { } @Override public STATUS status() { return STATUS.OPEN; } @Override public void init(final PwmApplication pwmApplication) throws PwmException { this.pwmApplication = pwmApplication; this.executor = createExecutor(pwmApplication); this.periodicDebugOutput(); } @Override public void close() { if (executor != null) { executor.shutdown(); } executor = null; } @Override public List<HealthRecord> healthCheck() { return Collections.emptyList(); } @Override public ServiceInfo serviceInfo() { return new ServiceInfo(Collections.emptyList()); } public UserIdentity resolveUsername( final String username, final String context, final String profile, final SessionLabel sessionLabel ) throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException { //check if username is a key { UserIdentity inputIdentity = null; try { inputIdentity = UserIdentity.fromKey(username, pwmApplication); } catch (PwmException e) { /* input is not a userIdentity */ } if (inputIdentity != null) { try { final ChaiUser theUser = pwmApplication.getProxiedChaiUser(inputIdentity); if (theUser.isValid()) { final String canonicalDN; canonicalDN = theUser.readCanonicalDN(); return new UserIdentity(canonicalDN, inputIdentity.getLdapProfileID()); } } catch (ChaiOperationException e) { throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER, e.getMessage())); } } } try { //see if we need to do a contextless search. if (checkIfStringIsDN(username, sessionLabel)) { return resolveUserDN(username); } else { final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder(); builder.username(username); if (context != null) { builder.contexts(Collections.singletonList(context)); } if (profile != null) { builder.ldapProfile(profile); } final SearchConfiguration searchConfiguration = builder.build(); return performSingleUserSearch(searchConfiguration, sessionLabel); } } catch (PwmOperationalException e) { throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER,e.getErrorInformation().getDetailedErrorMsg(),e.getErrorInformation().getFieldValues())); } } public UserIdentity performSingleUserSearch( final SearchConfiguration searchConfiguration, final SessionLabel sessionLabel ) throws PwmUnrecoverableException, PwmOperationalException { final long startTime = System.currentTimeMillis(); final DuplicateMode dupeMode = pwmApplication.getConfig().readSettingAsEnum(PwmSetting.LDAP_DUPLICATE_MODE, DuplicateMode.class); final int searchCount = (dupeMode == DuplicateMode.FIRST_ALL) ? 1 : 2; final Map<UserIdentity,Map<String,String>> searchResults = performMultiUserSearch(searchConfiguration, searchCount, Collections.emptyList(), sessionLabel); final List<UserIdentity> results = searchResults == null ? Collections.emptyList() : new ArrayList<>(searchResults.keySet()); if (results.isEmpty()) { final String errorMessage; if (searchConfiguration.getUsername() != null && searchConfiguration.getUsername().length() > 0) { errorMessage = "an ldap user for username value '" + searchConfiguration.getUsername() + "' was not found"; } else { errorMessage = "an ldap user was not found"; } throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER,errorMessage)); } else if (results.size() == 1) { final String userDN = results.get(0).getUserDN(); LOGGER.debug(sessionLabel, "found userDN: " + userDN + " (" + TimeDuration.fromCurrent(startTime).asCompactString() + ")"); return results.get(0); } if (dupeMode == DuplicateMode.FIRST_PROFILE) { final String profile1 = results.get(0).getLdapProfileID(); final String profile2 = results.get(1).getLdapProfileID(); if (profile1 == null && profile2 == null || (profile1 != null && profile1.equals(profile2))) { return results.get(0); } else { final String errorMessage = "multiple user matches in single profile"; throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER, errorMessage)); } } final String errorMessage = "multiple user matches found"; throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER, errorMessage)); } public UserSearchResults performMultiUserSearchFromForm( final Locale locale, final SearchConfiguration searchConfiguration, final int maxResults, final List<FormConfiguration> formItem, final SessionLabel sessionLabel ) throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException { final Map<String,String> attributeHeaderMap = UserSearchResults.fromFormConfiguration(formItem,locale); final Map<UserIdentity,Map<String,String>> searchResults = performMultiUserSearch( searchConfiguration, maxResults + 1, attributeHeaderMap.keySet(), sessionLabel ); final boolean resultsExceeded = searchResults.size() > maxResults; final Map<UserIdentity,Map<String,String>> returnData = new LinkedHashMap<>(); for (final UserIdentity loopUser : searchResults.keySet()) { returnData.put(loopUser, searchResults.get(loopUser)); if (returnData.size() >= maxResults) { break; } } return new UserSearchResults(attributeHeaderMap,returnData,resultsExceeded); } public Map<UserIdentity,Map<String,String>> performMultiUserSearch( final SearchConfiguration searchConfiguration, final int maxResults, final Collection<String> returnAttributes, final SessionLabel sessionLabel ) throws PwmUnrecoverableException, PwmOperationalException { final Collection<LdapProfile> ldapProfiles; if (searchConfiguration.getLdapProfile() != null && !searchConfiguration.getLdapProfile().isEmpty()) { if (pwmApplication.getConfig().getLdapProfiles().containsKey(searchConfiguration.getLdapProfile())) { ldapProfiles = Collections.singletonList(pwmApplication.getConfig().getLdapProfiles().get(searchConfiguration.getLdapProfile())); } else { LOGGER.debug(sessionLabel, "attempt to search for users in unknown ldap profile '" + searchConfiguration.getLdapProfile() + "', skipping search"); return Collections.emptyMap(); } } else { ldapProfiles = pwmApplication.getConfig().getLdapProfiles().values(); } final boolean ignoreUnreachableProfiles = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.LDAP_IGNORE_UNREACHABLE_PROFILES); final List<String> errors = new ArrayList<>(); final long profileRetryDelayMS = Long.valueOf(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PROFILE_RETRY_DELAY)); final List<UserSearchJob> searchJobs = new ArrayList<>(); for (final LdapProfile ldapProfile : ldapProfiles) { boolean skipProfile = false; final Instant lastLdapFailure = pwmApplication.getLdapConnectionService().getLastLdapFailureTime(ldapProfile); if (ldapProfiles.size() > 1 && lastLdapFailure != null && TimeDuration.fromCurrent(lastLdapFailure).isShorterThan(profileRetryDelayMS)) { LOGGER.info("skipping user search on ldap profile " + ldapProfile.getIdentifier() + " due to recent unreachable status (" + TimeDuration.fromCurrent(lastLdapFailure).asCompactString() + ")"); skipProfile = true; } if (!skipProfile) { try { searchJobs.addAll(this.makeSearchJobs( ldapProfile, searchConfiguration, maxResults, returnAttributes )); } catch (PwmUnrecoverableException e) { if (e.getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE) { pwmApplication.getLdapConnectionService().setLastLdapFailure(ldapProfile,e.getErrorInformation()); if (ignoreUnreachableProfiles) { errors.add(e.getErrorInformation().getDetailedErrorMsg()); if (errors.size() >= ldapProfiles.size()) { final String errorMsg = "all ldap profiles are unreachable; errors: " + JsonUtil.serializeCollection(errors); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, errorMsg)); } } } else { throw e; } } } } final Map<UserIdentity,Map<String,String>> resultsMap = new LinkedHashMap<>(executeSearchJobs(searchJobs, sessionLabel, searchCounter.getAndIncrement())); final Map<UserIdentity,Map<String,String>> returnMap = trimOrderedMap(resultsMap, maxResults); return Collections.unmodifiableMap(returnMap); } private Collection<UserSearchJob> makeSearchJobs( final LdapProfile ldapProfile, final SearchConfiguration searchConfiguration, final int maxResults, final Collection<String> returnAttributes ) throws PwmUnrecoverableException, PwmOperationalException { // check the search configuration data params searchConfiguration.validate(); final String input_searchFilter = searchConfiguration.getFilter() != null && searchConfiguration.getFilter().length() > 1 ? searchConfiguration.getFilter() : ldapProfile.readSettingAsString(PwmSetting.LDAP_USERNAME_SEARCH_FILTER); final String searchFilter; if (searchConfiguration.getUsername() != null) { final String inputQuery = searchConfiguration.isEnableValueEscaping() ? StringUtil.escapeLdapFilter(searchConfiguration.getUsername()) : searchConfiguration.getUsername(); if (searchConfiguration.isEnableSplitWhitespace() && (searchConfiguration.getUsername().split("\\s").length > 1)) { // split on all whitespace chars final StringBuilder multiSearchFilter = new StringBuilder(); multiSearchFilter.append("(&"); for (final String queryPart : searchConfiguration.getUsername().split(" ")) { multiSearchFilter.append("("); multiSearchFilter.append(input_searchFilter.replace(PwmConstants.VALUE_REPLACEMENT_USERNAME, queryPart)); multiSearchFilter.append(")"); } multiSearchFilter.append(")"); searchFilter = multiSearchFilter.toString(); } else { searchFilter = input_searchFilter.replace(PwmConstants.VALUE_REPLACEMENT_USERNAME, inputQuery.trim()); } } else if (searchConfiguration.getGroupDN() != null) { final String groupAttr = ldapProfile.readSettingAsString(PwmSetting.LDAP_USER_GROUP_ATTRIBUTE); searchFilter = "(" + groupAttr + "=" + searchConfiguration.getGroupDN() + ")"; } else if (searchConfiguration.getFormValues() != null) { searchFilter = figureSearchFilterForParams(searchConfiguration.getFormValues(),input_searchFilter,searchConfiguration.isEnableValueEscaping()); } else { searchFilter = input_searchFilter; } final List<String> searchContexts; if (searchConfiguration.getContexts() != null && !searchConfiguration.getContexts().isEmpty() && searchConfiguration.getContexts().iterator().next() != null && searchConfiguration.getContexts().iterator().next().length() > 0 ) { searchContexts = searchConfiguration.getContexts(); if (searchConfiguration.isEnableContextValidation()) { for (final String searchContext : searchContexts) { validateSpecifiedContext(ldapProfile, searchContext); } } } else { searchContexts = ldapProfile.getRootContexts(pwmApplication); } final long timeLimitMS = searchConfiguration.getSearchTimeout() != 0 ? searchConfiguration.getSearchTimeout() : (ldapProfile.readSettingAsLong(PwmSetting.LDAP_SEARCH_TIMEOUT) * 1000); final ChaiProvider chaiProvider = searchConfiguration.getChaiProvider() == null ? pwmApplication.getProxyChaiProvider(ldapProfile.getIdentifier()) : searchConfiguration.getChaiProvider(); final List<UserSearchJob> returnMap = new ArrayList<>(); for (final String loopContext : searchContexts) { final UserSearchJob userSearchJob = UserSearchJob.builder() .ldapProfile(ldapProfile) .searchFilter(searchFilter) .context(loopContext) .returnAttributes(returnAttributes) .maxResults(maxResults) .chaiProvider(chaiProvider) .timeoutMs(timeLimitMS) .build(); returnMap.add(userSearchJob); } return returnMap; } private Map<UserIdentity,Map<String,String>> executeSearch( final UserSearchJob userSearchJob, final SessionLabel sessionLabel, final int searchID, final int jobID ) throws PwmOperationalException, PwmUnrecoverableException { debugOutputTask.conditionallyExecuteTask(); final SearchHelper searchHelper = new SearchHelper(); searchHelper.setMaxResults(userSearchJob.getMaxResults()); searchHelper.setFilter(userSearchJob.getSearchFilter()); searchHelper.setAttributes(userSearchJob.getReturnAttributes()); searchHelper.setTimeLimit((int)userSearchJob.getTimeoutMs()); final String debugInfo; { final Map<String,String> props = new LinkedHashMap<>(); props.put("profile", userSearchJob.getLdapProfile().getIdentifier()); props.put("base", userSearchJob.getContext()); props.put("maxCount", String.valueOf(searchHelper.getMaxResults())); debugInfo = "[" + StringUtil.mapToString(props) + "]"; } log(PwmLogLevel.TRACE, sessionLabel, searchID, jobID, "performing ldap search for user; " + debugInfo); final Instant startTime = Instant.now(); final Map<String, Map<String,String>> results; try { results = userSearchJob.getChaiProvider().search(userSearchJob.getContext(), searchHelper); } catch (ChaiUnavailableException e) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE,e.getMessage())); } catch (ChaiOperationException e) { throw new PwmOperationalException(PwmError.forChaiError(e.getErrorCode()),"ldap error during searchID=" + searchID + ", error=" + e.getMessage()); } final TimeDuration searchDuration = TimeDuration.fromCurrent(startTime); if (pwmApplication.getStatisticsManager() != null && pwmApplication.getStatisticsManager().status() == PwmService.STATUS.OPEN) { pwmApplication.getStatisticsManager().updateAverageValue(Statistic.AVG_LDAP_SEARCH_TIME, searchDuration.getTotalMilliseconds()); } if (results.isEmpty()) { log(PwmLogLevel.TRACE, sessionLabel, searchID, jobID, "no matches from search (" + searchDuration.asCompactString() +"); " + debugInfo); return Collections.emptyMap(); } log(PwmLogLevel.TRACE, sessionLabel, searchID, jobID, "found " + results.size() + " results in " + searchDuration.asCompactString() + "; " + debugInfo); final Map<UserIdentity,Map<String,String>> returnMap = new LinkedHashMap<>(); for (final String userDN : results.keySet()) { final UserIdentity userIdentity = new UserIdentity(userDN, userSearchJob.getLdapProfile().getIdentifier()); final Map<String,String> attributeMap = results.get(userDN); returnMap.put(userIdentity, attributeMap); } return returnMap; } private void validateSpecifiedContext(final LdapProfile profile, final String context) throws PwmOperationalException, PwmUnrecoverableException { final Map<String,String> selectableContexts = profile.getSelectableContexts(pwmApplication); if (selectableContexts == null || selectableContexts.isEmpty()) { throw new PwmOperationalException(PwmError.ERROR_UNKNOWN,"context specified, but no selectable contexts are configured"); } for (final String loopContext : selectableContexts.keySet()) { if (loopContext.equals(context)) { return; } } throw new PwmOperationalException(PwmError.ERROR_UNKNOWN,"context '" + context + "' is specified, but is not in configuration"); } private boolean checkIfStringIsDN( final String input, final SessionLabel sessionLabel ) { if (input == null || input.length() < 1) { return false; } //if supplied user name starts with username attr assume its the full dn and skip the search final Set<String> namingAttributes = new HashSet<>(); for (final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values()) { final String usernameAttribute = ldapProfile.readSettingAsString(PwmSetting.LDAP_NAMING_ATTRIBUTE); if (input.toLowerCase().startsWith(usernameAttribute.toLowerCase() + "=")) { LOGGER.trace(sessionLabel, "username '" + input + "' appears to be a DN (starts with configured ldap naming attribute'" + usernameAttribute + "'), skipping username search"); return true; } namingAttributes.add(usernameAttribute); } LOGGER.trace(sessionLabel, "username '" + input + "' does not appear to be a DN (does not start with any of the configured ldap naming attributes '" + StringUtil.collectionToString(namingAttributes,",") + "')"); return false; } private UserIdentity resolveUserDN( final String userDN ) throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException { final Collection<LdapProfile> ldapProfiles = pwmApplication.getConfig().getLdapProfiles().values(); for (final LdapProfile ldapProfile : ldapProfiles) { final ChaiProvider provider = pwmApplication.getProxyChaiProvider(ldapProfile.getIdentifier()); final ChaiUser user = ChaiFactory.createChaiUser(userDN, provider); if (user.isValid()) { try { return new UserIdentity(user.readCanonicalDN(), ldapProfile.getIdentifier()); } catch (ChaiOperationException e) { LOGGER.error("unexpected error reading canonical userDN for '" + userDN + "', error: " + e.getMessage()); } } } throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER)); } private Map<UserIdentity,Map<String,String>> executeSearchJobs( final Collection<UserSearchJob> userSearchJobs, final SessionLabel sessionLabel, final int searchID ) throws PwmUnrecoverableException { // create jobs final List<JobInfo> jobs = new ArrayList<>(); { int jobID = 0; for (UserSearchJob userSearchJob : userSearchJobs) { final int loopJobID = jobID++; final FutureTask<Map<UserIdentity, Map<String, String>>> futureTask = new FutureTask<>(() -> executeSearch(userSearchJob, sessionLabel, searchID, loopJobID)); final JobInfo jobInfo = new JobInfo(searchID, loopJobID, userSearchJob, futureTask); jobs.add(jobInfo); } } final Instant startTime = Instant.now(); { final String filterText = jobs.isEmpty() ? "" : ", filter: " + jobs.iterator().next().getUserSearchJob().getSearchFilter(); log(PwmLogLevel.DEBUG, sessionLabel, searchID, -1, "beginning user search process with " + jobs.size() + " search jobs" + filterText); } // execute jobs for (Iterator<JobInfo> iterator = jobs.iterator(); iterator.hasNext(); ) { final JobInfo jobInfo = iterator.next(); boolean submittedToExecutor = false; // use current thread to execute one (the last in the loop) task. if (executor != null && iterator.hasNext()) { try { executor.submit(jobInfo.getFutureTask()); submittedToExecutor = true; backgroundJobCounter.incrementAndGet(); } catch (RejectedExecutionException e) { // executor is full, so revert to running locally rejectionJobCounter.incrementAndGet(); } } if (!submittedToExecutor) { try { jobInfo.getFutureTask().run(); foregroundJobCounter.incrementAndGet(); } catch (Throwable t) { log(PwmLogLevel.ERROR, sessionLabel, searchID, jobInfo.getJobID(), "unexpected error running job in local thread: " + t.getMessage()); } } } // aggregate results final Map<UserIdentity,Map<String,String>> results = new LinkedHashMap<>(); for (final JobInfo jobInfo : jobs) { if (results.size() > jobInfo.getUserSearchJob().getMaxResults()) { final FutureTask futureTask = jobInfo.getFutureTask(); if (!futureTask.isDone()) { canceledJobCounter.incrementAndGet(); } jobInfo.getFutureTask().cancel(false); } else { final long maxWaitTime = jobInfo.getUserSearchJob().getTimeoutMs() * 3; try { results.putAll(jobInfo.getFutureTask().get(maxWaitTime, TimeUnit.MILLISECONDS)); } catch (InterruptedException e) { final String errorMsg = "unexpected interruption during search job execution: " + e.getMessage(); log(PwmLogLevel.WARN, sessionLabel, searchID, jobInfo.getJobID(), errorMsg); LOGGER.error(sessionLabel, errorMsg, e); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg)); } catch (ExecutionException e) { final Throwable t = e.getCause(); final ErrorInformation errorInformation; final String errorMsg = "unexpected error during ldap search (" + "profile=" + jobInfo.getUserSearchJob().getLdapProfile() + ")" + ", error: " + (t instanceof PwmException ? t.getMessage() : JavaHelper.readHostileExceptionMessage(t)); if (t instanceof PwmException) { errorInformation = new ErrorInformation(((PwmException) t).getError(), errorMsg); } else { errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg); } log(PwmLogLevel.WARN, sessionLabel, searchID, jobInfo.getJobID(), "error during user search: " + errorInformation.toDebugStr()); throw new PwmUnrecoverableException(errorInformation); } catch (TimeoutException e) { final String errorMsg = "background search job timeout after " + jobInfo.getUserSearchJob().getTimeoutMs() + "ms, to ldapProfile '" + jobInfo.getUserSearchJob().getLdapProfile() + "'"; log(PwmLogLevel.WARN, sessionLabel, searchID, jobInfo.getJobID(), "error during user search: " + errorMsg); jobTimeoutCounter.incrementAndGet(); } } } log(PwmLogLevel.DEBUG, sessionLabel, searchID, -1, "completed user search process in " + TimeDuration.fromCurrent(startTime).asCompactString() + ", intermediate result size=" + results.size()); return Collections.unmodifiableMap(results); } @Getter @AllArgsConstructor private static class JobInfo { private final int searchID; private final int jobID; private final UserSearchJob userSearchJob; private final FutureTask<Map<UserIdentity,Map<String,String>>> futureTask; } private Map<String,String> debugProperties() { final Map<String,String> properties = new TreeMap<>(); properties.put("searchCount", this.searchCounter.toString()); properties.put("backgroundJobCounter", Integer.toString(this.backgroundJobCounter.get())); properties.put("foregroundJobCounter", Integer.toString(this.foregroundJobCounter.get())); properties.put("jvmThreadCount", Integer.toString(Thread.activeCount())); if (executor == null) { properties.put("background-enabled","false"); } else { properties.put("background-enabled","true"); properties.put("background-maxPoolSize", Integer.toString(executor.getMaximumPoolSize())); properties.put("background-activeCount", Integer.toString(executor.getActiveCount())); properties.put("background-largestPoolSize", Integer.toString(executor.getLargestPoolSize())); properties.put("background-poolSize", Integer.toString(executor.getPoolSize())); properties.put("background-queue-size", Integer.toString(executor.getQueue().size())); properties.put("background-rejectionJobCounter", Integer.toString(rejectionJobCounter.get())); properties.put("background-canceledJobCounter", Integer.toString(canceledJobCounter.get())); properties.put("background-jobTimeoutCounter", Integer.toString(jobTimeoutCounter.get())); } return Collections.unmodifiableMap(properties); } private void periodicDebugOutput() { LOGGER.debug("periodic debug status: " + StringUtil.mapToString(debugProperties())); } private void log(final PwmLogLevel level, final SessionLabel sessionLabel, final int searchID, final int jobID, final String message) { final String idMsg = logIdString(searchID, jobID); LOGGER.log(level, sessionLabel, idMsg + " " + message); } private static String logIdString(final int searchID, final int jobID) { String idMsg = "searchID=" + searchID; if (jobID >= 0) { idMsg += "-" + jobID; } return idMsg; } private static ThreadPoolExecutor createExecutor(final PwmApplication pwmApplication) { final Configuration configuration = pwmApplication.getConfig(); final boolean enabled = Boolean.parseBoolean(configuration.readAppProperty(AppProperty.LDAP_SEARCH_PARALLEL_ENABLE)); if (!enabled) { return null; } final int endPoints; { int counter = 0; for (final LdapProfile ldapProfile : configuration.getLdapProfiles().values()) { final List<String> rootContexts = ldapProfile.readSettingAsStringArray(PwmSetting.LDAP_CONTEXTLESS_ROOT); counter += rootContexts.size(); } endPoints = counter; } if (endPoints > 1) { final int factor = Integer.parseInt(configuration.readAppProperty(AppProperty.LDAP_SEARCH_PARALLEL_FACTOR)); final int maxThreads = Integer.parseInt(configuration.readAppProperty(AppProperty.LDAP_SEARCH_PARALLEL_THREAD_MAX)); final int threads = Math.min(maxThreads, (endPoints) * factor); final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory(JavaHelper.makeThreadName(pwmApplication, UserSearchEngine.class), true); return new ThreadPoolExecutor( threads, threads, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(threads), threadFactory ); } return null; } private static <K,V> Map<K,V> trimOrderedMap(final Map<K,V> inputMap, final int maxEntries) { final Map<K,V> returnMap = new LinkedHashMap<>(inputMap); if (returnMap.size() > maxEntries) { int counter = 0; for (final Iterator<K> iterator = returnMap.keySet().iterator() ; iterator.hasNext(); ) { iterator.next(); counter++; if (counter > maxEntries) { iterator.remove(); } } } return Collections.unmodifiableMap(returnMap); } private static String figureSearchFilterForParams( final Map<FormConfiguration, String> formValues, final String searchFilter, final boolean enableValueEscaping ) { String newSearchFilter = searchFilter; for (final FormConfiguration formItem : formValues.keySet()) { final String attrName = "%" + formItem.getName() + "%"; String value = formValues.get(formItem); if (enableValueEscaping) { value = StringUtil.escapeLdapFilter(value); } newSearchFilter = newSearchFilter.replace(attrName, value); } return newSearchFilter; } }