/* * 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.util.operations; import com.novell.ldapchai.ChaiUser; import com.novell.ldapchai.cr.ChaiChallenge; import com.novell.ldapchai.cr.ChaiChallengeSet; import com.novell.ldapchai.cr.Challenge; import com.novell.ldapchai.cr.ChallengeSet; import com.novell.ldapchai.cr.ResponseSet; import com.novell.ldapchai.exception.ChaiException; import com.novell.ldapchai.exception.ChaiUnavailableException; import com.novell.ldapchai.exception.ChaiValidationException; import com.novell.ldapchai.impl.edir.NmasCrFactory; import com.novell.ldapchai.provider.ChaiProvider; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.bean.ResponseInfoBean; import password.pwm.bean.SessionLabel; import password.pwm.bean.UserIdentity; import password.pwm.config.Configuration; import password.pwm.config.PwmSetting; import password.pwm.config.UserPermission; import password.pwm.config.option.DataStorageMethod; import password.pwm.config.profile.ChallengeProfile; import password.pwm.config.profile.PwmPasswordPolicy; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmDataValidationException; 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.ldap.LdapOperationsHelper; import password.pwm.ldap.LdapPermissionTester; import password.pwm.svc.PwmService; import password.pwm.svc.wordlist.WordlistManager; import password.pwm.util.java.JsonUtil; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogger; import password.pwm.util.operations.cr.CrOperator; import password.pwm.util.operations.cr.DbCrOperator; import password.pwm.util.operations.cr.LdapCrOperator; import password.pwm.util.operations.cr.LocalDbCrOperator; import password.pwm.util.operations.cr.NMASCrOperator; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * @author Jason D. Rivard */ public class CrService implements PwmService { private static final PwmLogger LOGGER = PwmLogger.forClass(CrService.class); private final Map<DataStorageMethod,CrOperator> operatorMap = new HashMap<>(); private PwmApplication pwmApplication; public CrService() { } @Override public STATUS status() { return STATUS.OPEN; } @Override public void init(final PwmApplication pwmApplication) throws PwmException { this.pwmApplication = pwmApplication; operatorMap.put(DataStorageMethod.DB, new DbCrOperator(pwmApplication)); operatorMap.put(DataStorageMethod.LDAP, new LdapCrOperator(pwmApplication.getConfig())); operatorMap.put(DataStorageMethod.LOCALDB, new LocalDbCrOperator(pwmApplication.getLocalDB())); operatorMap.put(DataStorageMethod.NMAS, new NMASCrOperator(pwmApplication)); } @Override public void close() { for (final CrOperator operator : operatorMap.values()) { operator.close(); } operatorMap.clear(); } @Override public List<HealthRecord> healthCheck() { return Collections.emptyList(); } public ChallengeProfile readUserChallengeProfile( final SessionLabel sessionLabel, final UserIdentity userIdentity, final ChaiUser theUser, final PwmPasswordPolicy policy, final Locale locale ) throws PwmUnrecoverableException { final Configuration config = pwmApplication.getConfig(); final long methodStartTime = System.currentTimeMillis(); ChallengeSet returnSet = null; if (config.readSettingAsBoolean(PwmSetting.EDIRECTORY_READ_CHALLENGE_SET)) { try { if (theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) { if (policy != null && policy.getChaiPasswordPolicy() != null) { returnSet = NmasCrFactory.readAssignedChallengeSet(theUser.getChaiProvider(), policy.getChaiPasswordPolicy(), locale); } if (returnSet == null) { returnSet = NmasCrFactory.readAssignedChallengeSet(theUser, locale); } if (returnSet == null) { LOGGER.debug(sessionLabel,"no nmas c/r policy found for user " + theUser.getEntryDN()); } else { LOGGER.debug(sessionLabel,"using nmas c/r policy for user " + theUser.getEntryDN() + ": " + returnSet.toString()); final String challengeID = "nmasPolicy-" + userIdentity.toDelimitedKey(); final ChallengeProfile challengeProfile = ChallengeProfile.createChallengeProfile( challengeID, locale, applyPwmPolicyToNmasChallenges(returnSet, config), null, (int)config.readSettingAsLong(PwmSetting.EDIRECTORY_CR_MIN_RANDOM_DURING_SETUP), 0 ); LOGGER.debug(sessionLabel,"using ldap c/r policy for user " + theUser.getEntryDN() + ": " + returnSet.toString()); LOGGER.trace(sessionLabel,"readUserChallengeProfile completed in " + TimeDuration.fromCurrent(methodStartTime).asCompactString() + ", result=" + JsonUtil.serialize(challengeProfile)); return challengeProfile; } } } catch (ChaiException e) { LOGGER.error(sessionLabel,"error reading nmas c/r policy for user " + theUser.getEntryDN() + ": " + e.getMessage()); } LOGGER.debug(sessionLabel,"no detected c/r policy for user " + theUser.getEntryDN() + " in nmas"); } // use PWM policies if PWM is configured and either its all that is configured OR the NMAS policy read was not successful final String challengeProfileID = determineChallengeProfileForUser(pwmApplication, sessionLabel, userIdentity, locale); final ChallengeProfile challengeProfile = config.getChallengeProfile(challengeProfileID, locale); LOGGER.trace(sessionLabel,"readUserChallengeProfile completed in " + TimeDuration.fromCurrent(methodStartTime).asCompactString() + " returned profile: " + (challengeProfile == null ? "null" : challengeProfile.getIdentifier())); return challengeProfile; } private static ChallengeSet applyPwmPolicyToNmasChallenges(final ChallengeSet challengeSet, final Configuration configuration) throws PwmUnrecoverableException { final List<Challenge> newChallenges = new ArrayList<>(); final boolean applyWordlist = configuration.readSettingAsBoolean(PwmSetting.EDIRECTORY_CR_APPLY_WORDLIST); final int questionsInAnswer = (int)configuration.readSettingAsLong(PwmSetting.EDIRECTORY_CR_MAX_QUESTION_CHARS_IN__ANSWER); for (final Challenge challenge : challengeSet.getChallenges()) { newChallenges.add(new ChaiChallenge( challenge.isRequired(), challenge.getChallengeText(), challenge.getMinLength(), challenge.getMaxLength(), challenge.isAdminDefined(), questionsInAnswer, applyWordlist )); } try { return new ChaiChallengeSet( newChallenges, challengeSet.getMinRandomRequired(), challengeSet.getLocale(), challengeSet.getIdentifier() ); } catch (ChaiValidationException e) { final String errorMsg = "unexpected error applying policies to nmas challengeset: " + e.getMessage(); LOGGER.error(errorMsg,e); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,errorMsg)); } } protected static String determineChallengeProfileForUser( final PwmApplication pwmApplication, final SessionLabel sessionLabel, final UserIdentity userIdentity, final Locale locale ) throws PwmUnrecoverableException { final List<String> profiles = pwmApplication.getConfig().getChallengeProfileIDs(); if (profiles.isEmpty()) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_NO_PROFILE_ASSIGNED,"no challenge profile is configured")); } for (final String profile : profiles) { final ChallengeProfile loopPolicy = pwmApplication.getConfig().getChallengeProfile(profile, locale); final List<UserPermission> queryMatch = loopPolicy.getUserPermissions(); if (queryMatch != null && !queryMatch.isEmpty()) { LOGGER.debug(sessionLabel, "testing challenge profiles '" + profile + "'"); try { final boolean match = LdapPermissionTester.testUserPermissions(pwmApplication,sessionLabel,userIdentity,queryMatch); if (match) { return profile; } } catch (PwmUnrecoverableException e) { LOGGER.error(sessionLabel, "unexpected error while testing password policy profile '" + profile + "', error: " + e.getMessage()); } } } throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_NO_PROFILE_ASSIGNED,"no challenge profile is assigned")); } public void validateResponses( final ChallengeSet challengeSet, final Map<Challenge, String> responseMap, final int minRandomRequiredSetup ) throws PwmDataValidationException, PwmUnrecoverableException { //strip null keys from responseMap; for (final Iterator<Challenge> iter = responseMap.keySet().iterator(); iter.hasNext(); ) { final Challenge loopChallenge = iter.next(); if (loopChallenge == null) { iter.remove(); } } { // check for missing question texts for (final Challenge challenge : responseMap.keySet()) { if (!challenge.isAdminDefined()) { final String text = challenge.getChallengeText(); if (text == null || text.length() < 1) { final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_CHALLENGE_TEXT); throw new PwmDataValidationException(errorInformation); } } } } final Configuration config = pwmApplication.getConfig(); { // check responses against wordlist final WordlistManager wordlistManager = pwmApplication.getWordlistManager(); if (wordlistManager.status() == PwmService.STATUS.OPEN) { for (final Challenge loopChallenge : responseMap.keySet()) { if (loopChallenge.isEnforceWordlist()) { final String answer = responseMap.get(loopChallenge); if (wordlistManager.containsWord(answer)) { final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_RESPONSE_WORDLIST, null, new String[]{loopChallenge.getChallengeText()}); throw new PwmDataValidationException(errorInfo); } } } } } { // check for duplicate questions. need to check the actual req params because the following dupes wont populate duplicates final Set<String> userQuestionTexts = new HashSet<>(); for (final Challenge challenge : responseMap.keySet()) { final String text = challenge.getChallengeText(); if (text != null) { if (userQuestionTexts.contains(text.toLowerCase())) { final String errorMsg = "duplicate challenge text: " + text; final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_CHALLENGE_DUPLICATE, errorMsg, new String[]{text}); throw new PwmDataValidationException(errorInformation); } else { userQuestionTexts.add(text.toLowerCase()); } } } } int randomCount = 0; for (final Challenge loopChallenge : responseMap.keySet()) { if (!loopChallenge.isRequired()) { randomCount++; } } if (minRandomRequiredSetup == 0) { // if using recover style, then all readResponseSet must be supplied at this point. if (randomCount < challengeSet.getRandomChallenges().size()) { final String errorMsg = "all randoms required, but not all randoms are completed"; final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_MISSING_RANDOM_RESPONSE, errorMsg); throw new PwmDataValidationException(errorInfo); } } if (randomCount < minRandomRequiredSetup) { final String errorMsg = minRandomRequiredSetup + " randoms required, but not only " + randomCount + " randoms are completed"; final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_MISSING_RANDOM_RESPONSE, errorMsg); throw new PwmDataValidationException(errorInfo); } if (responseMap == null || responseMap.isEmpty()) { final String errorMsg = "empty response set"; final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER, errorMsg); throw new PwmDataValidationException(errorInfo); } } public ResponseInfoBean readUserResponseInfo( final SessionLabel sessionLabel, final UserIdentity userIdentity, final ChaiUser theUser ) throws ChaiUnavailableException, PwmUnrecoverableException { final Configuration config = pwmApplication.getConfig(); LOGGER.trace(sessionLabel, "beginning read of user response sequence"); final List<DataStorageMethod> readPreferences = config.helper().getCrReadPreference(); final String debugMsg = "will attempt to read the following storage methods: " + JsonUtil.serializeCollection(readPreferences) + " for response info for user " + theUser.getEntryDN(); LOGGER.debug(sessionLabel, debugMsg); final String userGUID; if (readPreferences.contains(DataStorageMethod.DB) || readPreferences.contains(DataStorageMethod.LOCALDB)) { userGUID = LdapOperationsHelper.readLdapGuidValue(pwmApplication, sessionLabel, userIdentity, false); } else { userGUID = null; } for (final DataStorageMethod storageMethod : readPreferences) { final ResponseInfoBean readResponses; LOGGER.trace(sessionLabel, "attempting read of response info via storage method: " + storageMethod); readResponses = operatorMap.get(storageMethod).readResponseInfo(theUser, userIdentity, userGUID); if (readResponses != null) { LOGGER.debug(sessionLabel,"returning response info read via method " + storageMethod + " for user " + theUser.getEntryDN()); return readResponses; } else { LOGGER.trace(sessionLabel, "no responses info read using method " + storageMethod); } } LOGGER.debug(sessionLabel,"no response info found for user " + theUser.getEntryDN()); return null; } public ResponseSet readUserResponseSet( final SessionLabel sessionLabel, final UserIdentity userIdentity, final ChaiUser theUser ) throws ChaiUnavailableException, PwmUnrecoverableException { final Configuration config = pwmApplication.getConfig(); LOGGER.trace(sessionLabel, "beginning read of user response sequence"); final List<DataStorageMethod> readPreferences = config.helper().getCrReadPreference(); final String debugMsg = "will attempt to read the following storage methods: " + JsonUtil.serializeCollection(readPreferences) + " for user " + theUser.getEntryDN(); LOGGER.debug(sessionLabel, debugMsg); final String userGUID; if (readPreferences.contains(DataStorageMethod.DB) || readPreferences.contains(DataStorageMethod.LOCALDB)) { userGUID = LdapOperationsHelper.readLdapGuidValue(pwmApplication, sessionLabel, userIdentity, false); } else { userGUID = null; } for (final DataStorageMethod storageMethod : readPreferences) { final ResponseSet readResponses; LOGGER.trace(sessionLabel, "attempting read of responses via storage method: " + storageMethod); readResponses = operatorMap.get(storageMethod).readResponseSet(theUser, userIdentity, userGUID); if (readResponses != null) { LOGGER.debug(sessionLabel,"returning responses read via method " + storageMethod + " for user " + theUser.getEntryDN()); return readResponses; } else { LOGGER.trace(sessionLabel, "no responses read using method " + storageMethod); } } LOGGER.debug(sessionLabel,"no responses found for user " + theUser.getEntryDN()); return null; } public void writeResponses( final UserIdentity userIdentity, final ChaiUser theUser, final String userGUID, final ResponseInfoBean responseInfoBean ) throws PwmOperationalException, ChaiUnavailableException, ChaiValidationException { int attempts = 0; int successes = 0; final Map<DataStorageMethod,String> errorMessages = new LinkedHashMap<>(); final Configuration config = pwmApplication.getConfig(); final List<DataStorageMethod> writeMethods = config.helper().getCrWritePreference(); for (final DataStorageMethod loopWriteMethod : writeMethods) { try { attempts++; operatorMap.get(loopWriteMethod).writeResponses(userIdentity, theUser, userGUID, responseInfoBean); LOGGER.debug("saved responses using storage method " + loopWriteMethod + " for user " + theUser.getEntryDN()); errorMessages.put(loopWriteMethod,"Success"); successes++; } catch (PwmUnrecoverableException e) { final String errorMsg = "error saving responses via " + loopWriteMethod + ", error: " + e.getMessage(); errorMessages.put(loopWriteMethod,errorMsg); LOGGER.error(errorMsg); } } if (attempts == 0) { final String errorMsg = "no response save methods are available or configured"; final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_WRITING_RESPONSES, errorMsg); throw new PwmOperationalException(errorInfo); } if (attempts != successes) { final String errorMsg = "response storage only partially successful; attempts=" + attempts + ", successes=" + successes + ", detail=" + JsonUtil.serializeMap(errorMessages); final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_WRITING_RESPONSES, errorMsg); throw new PwmOperationalException(errorInfo); } } public void clearResponses( final SessionLabel sessionLabel, final UserIdentity userIdentity, final ChaiUser theUser, final String userGUID ) throws PwmOperationalException, ChaiUnavailableException { final Configuration config = pwmApplication.getConfig(); int attempts = 0; int successes = 0; LOGGER.trace(sessionLabel, "beginning clear response operation for user " + theUser.getEntryDN() + " guid=" + userGUID); final List<DataStorageMethod> writeMethods = config.helper().getCrWritePreference(); for (final DataStorageMethod loopWriteMethod : writeMethods) { try { attempts++; operatorMap.get(loopWriteMethod).clearResponses(userIdentity, theUser, userGUID); successes++; } catch (PwmUnrecoverableException e) { LOGGER.error(sessionLabel, "error clearing responses via " + loopWriteMethod + ", error: " + e.getMessage()); } } if (attempts == 0) { final String errorMsg = "no response save methods are available or configured"; final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_CLEARING_RESPONSES, errorMsg); throw new PwmOperationalException(errorInfo); } if (attempts != successes) { // should be impossible to read here, but just in case. final String errorMsg = "response clear partially successful; attempts=" + attempts + ", successes=" + successes; final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_CLEARING_RESPONSES, errorMsg); throw new PwmOperationalException(errorInfo); } } public boolean checkIfResponseConfigNeeded( final PwmApplication pwmApplication, final SessionLabel pwmSession, final UserIdentity userIdentity, final ChallengeSet challengeSet, final ResponseInfoBean responseInfoBean ) throws ChaiUnavailableException, PwmUnrecoverableException { LOGGER.trace(pwmSession, "beginning check to determine if responses need to be configured for user"); final Configuration config = pwmApplication.getConfig(); if (!config.readSettingAsBoolean(PwmSetting.CHALLENGE_ENABLE)) { LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: response setup is disabled, so user is not required to setup responses"); return false; } if (!config.readSettingAsBoolean(PwmSetting.CHALLENGE_FORCE_SETUP)) { LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: force response setup is disabled, so user is not required to setup responses"); return false; } if (!LdapPermissionTester.testUserPermissions(pwmApplication, pwmSession, userIdentity, config.readSettingAsUserPermission(PwmSetting.QUERY_MATCH_SETUP_RESPONSE))) { LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: " + userIdentity + " does not have permission to setup responses"); return false; } if (!LdapPermissionTester.testUserPermissions(pwmApplication, pwmSession, userIdentity, config.readSettingAsUserPermission(PwmSetting.QUERY_MATCH_CHECK_RESPONSES))) { LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: " + userIdentity + " is not eligible for checkIfResponseConfigNeeded due to query match"); return false; } // check to be sure there are actually challenges in the challenge set if (challengeSet == null || challengeSet.getChallenges().isEmpty()) { LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: no challenge sets configured for user " + userIdentity); return false; } // ignore NMAS based CR set if so configured if (responseInfoBean != null && (responseInfoBean.getDataStorageMethod() == DataStorageMethod.NMAS)) { final boolean ignoreNmasCr = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_IGNORE_NMASCR_DURING_FORCECHECK)); if (ignoreNmasCr) { LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: app property " + AppProperty.NMAS_IGNORE_NMASCR_DURING_FORCECHECK.getKey() + "=true and user's responses are in " + responseInfoBean.getDataStorageMethod() + " format, so forcing setup of new responses."); return true; } } try { // check if responses exist if (responseInfoBean == null) { throw new Exception("no responses configured"); } // check if responses meet the challenge set policy for the user //usersResponses.meetsChallengeSetRequirements(challengeSet); LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: " + userIdentity + " has good responses"); return false; } catch (Exception e) { LOGGER.debug(pwmSession, "checkIfResponseConfigNeeded: " + userIdentity + " does not have good responses: " + e.getMessage()); return true; } } @Override public ServiceInfo serviceInfo() { final LinkedHashSet<DataStorageMethod> usedStorageMethods = new LinkedHashSet<>(); usedStorageMethods.addAll(pwmApplication.getConfig().helper().getCrReadPreference()); usedStorageMethods.addAll(pwmApplication.getConfig().helper().getCrWritePreference()); return new ServiceInfo(Collections.unmodifiableList(new ArrayList(usedStorageMethods))); } }