/* * 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.cr; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPException; import com.novell.ldapchai.ChaiFactory; 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.cr.bean.ChallengeBean; import com.novell.ldapchai.exception.ChaiError; import com.novell.ldapchai.exception.ChaiException; import com.novell.ldapchai.exception.ChaiOperationException; import com.novell.ldapchai.exception.ChaiUnavailableException; import com.novell.ldapchai.exception.ChaiValidationException; import com.novell.ldapchai.impl.edir.NmasCrFactory; import com.novell.ldapchai.impl.edir.NmasResponseSet; import com.novell.ldapchai.provider.ChaiConfiguration; import com.novell.ldapchai.provider.ChaiProvider; import com.novell.ldapchai.provider.ChaiProviderFactory; import com.novell.ldapchai.provider.ChaiProviderImplementor; import com.novell.ldapchai.provider.ChaiSetting; import com.novell.ldapchai.provider.JLDAPProviderImpl; import com.novell.security.nmas.client.NMASCallback; import com.novell.security.nmas.client.NMASCompletionCallback; import com.novell.security.nmas.lcm.LCMEnvironment; import com.novell.security.nmas.lcm.LCMUserPrompt; import com.novell.security.nmas.lcm.LCMUserPromptCallback; import com.novell.security.nmas.lcm.LCMUserPromptException; import com.novell.security.nmas.lcm.LCMUserResponse; import com.novell.security.nmas.lcm.registry.GenLCMRegistry; import com.novell.security.nmas.lcm.registry.LCMRegistry; import com.novell.security.nmas.lcm.registry.LCMRegistryException; import com.novell.security.nmas.ui.GenLcmUI; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.ResponseInfoBean; import password.pwm.bean.UserIdentity; import password.pwm.config.Configuration; import password.pwm.config.PwmSetting; import password.pwm.config.option.DataStorageMethod; 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.PwmUnrecoverableException; import password.pwm.ldap.LdapOperationsHelper; import password.pwm.util.PasswordData; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.JsonUtil; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogger; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.sasl.SaslClient; import javax.security.sasl.SaslClientFactory; import javax.security.sasl.SaslException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.IOException; import java.io.Serializable; import java.io.StringReader; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.Provider; import java.security.Security; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.TreeMap; public class NMASCrOperator implements CrOperator { private static final PwmLogger LOGGER = PwmLogger.forClass(NMASCrOperator.class); private int threadCounter = 0; private final List<NMASSessionThread> sessionMonitorThreads = Collections.synchronizedList(new ArrayList<NMASSessionThread>()); private final PwmApplication pwmApplication; private final TimeDuration maxThreadIdleTime; private final int maxThreadCount; private volatile Timer timer; private Provider saslProvider; private static final Map<String,Object> CR_OPTIONS_MAP; static { final HashMap<String,Object> crOptionsMap = new HashMap<>(); crOptionsMap.put("com.novell.security.sasl.client.pkgs", "com.novell.sasl.client"); crOptionsMap.put("javax.security.sasl.client.pkgs", "com.novell.sasl.client"); crOptionsMap.put("LoginSequence", "Challenge Response"); CR_OPTIONS_MAP = Collections.unmodifiableMap(crOptionsMap); } public NMASCrOperator(final PwmApplication pwmApplication) { this.pwmApplication = pwmApplication; maxThreadCount = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_THREADS_MAX_COUNT)); final int MAX_SECONDS = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_THREADS_MAX_SECONDS)); final int MIN_SECONDS = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_THREADS_MIN_SECONDS)); int maxNmasIdleSeconds = (int)pwmApplication.getConfig().readSettingAsLong(PwmSetting.IDLE_TIMEOUT_SECONDS); if (maxNmasIdleSeconds > MAX_SECONDS) { maxNmasIdleSeconds = MAX_SECONDS; } else if (maxNmasIdleSeconds < MIN_SECONDS) { maxNmasIdleSeconds = MIN_SECONDS; } maxThreadIdleTime = new TimeDuration(maxNmasIdleSeconds * 1000); registerSaslProvider(); } private void registerSaslProvider() { final boolean forceRegistration = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_FORCE_SASL_FACTORY_REGISTRATION)); if (Security.getProvider(NMASCrPwmSaslProvider.SASL_PROVIDER_NAME) != null) { if (forceRegistration) { LOGGER.warn("SASL provider '" + NMASCrPwmSaslProvider.SASL_PROVIDER_NAME + "' is already defined, however forcing registration due to app property " + AppProperty.NMAS_FORCE_SASL_FACTORY_REGISTRATION.getKey() + " value"); } else { LOGGER.warn("SASL provider '" + NMASCrPwmSaslProvider.SASL_PROVIDER_NAME + "' is already defined, skipping SASL factory registration"); return; } } else { LOGGER.trace("pre-existing SASL provider for " + NMASCrPwmSaslProvider.SASL_PROVIDER_NAME + " has not been detected"); } final boolean useLocalProvider = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_USE_LOCAL_SASL_FACTORY)); try { if (useLocalProvider) { LOGGER.trace("registering built-in local SASL provider"); saslProvider = new NMASCrPwmSaslProvider(); } else { LOGGER.trace("registering NMAS library SASL provider"); saslProvider = new com.novell.sasl.client.NovellSaslProvider(); } LOGGER.trace("initialized security provider " + saslProvider.getClass().getName()); } catch (Throwable t) { LOGGER.warn("unable to create SASL provider, error: " + t.getMessage(), t); } if (saslProvider != null) { try { Security.addProvider(saslProvider); } catch (Exception e) { LOGGER.warn("error registering security provider"); } } } private void unregisterSaslProvider() { if (saslProvider != null) { saslProvider = null; try { Security.removeProvider(NMASCrPwmSaslProvider.SASL_PROVIDER_NAME); } catch (Exception e) { LOGGER.warn("error removing provider " + NMASCrPwmSaslProvider.SASL_PROVIDER_NAME + ", error: " + e.getMessage()); } } } private void controlWatchdogThread() { synchronized (sessionMonitorThreads) { if (sessionMonitorThreads.isEmpty()) { final Timer localTimer = timer; if (localTimer != null) { LOGGER.debug("discontinuing NMASCrOperator watchdog timer, no active threads"); localTimer.cancel(); timer = null; } } else { if (timer == null) { LOGGER.debug("starting NMASCrOperator watchdog timer, maxIdleThreadTime=" + maxThreadIdleTime.asCompactString()); timer = new Timer(PwmConstants.PWM_APP_NAME + "-NMASCrOperator watchdog timer",true); final long frequency = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_THREADS_WATCHDOG_FREQUENCY)); final boolean debugOutput = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_THREADS_WATCHDOG_DEBUG)); timer.schedule(new ThreadWatchdogTask(debugOutput),frequency,frequency); } } } } public void close() { unregisterSaslProvider(); final List<NMASSessionThread> threads = new ArrayList<>(sessionMonitorThreads); for (final NMASSessionThread thread : threads) { LOGGER.debug("killing thread due to NMASCrOperator service closing: " + thread.toDebugString()); thread.abort(); } } public ResponseSet readResponseSet( final ChaiUser theUser, final UserIdentity userIdentity, final String userGuid ) throws PwmUnrecoverableException { pwmApplication.getIntruderManager().convenience().checkUserIdentity(userIdentity); try { if (theUser.getChaiProvider().getDirectoryVendor() != ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) { return null; } final ResponseSet responseSet = new NMASCRResponseSet(pwmApplication, userIdentity); if (responseSet.getChallengeSet() == null) { return null; } return responseSet; } catch (PwmException e) { throw new PwmUnrecoverableException(e.getErrorInformation()); } catch (Exception e) { final String errorMsg = "unexpected error loading NMAS responses: " + e.getMessage(); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_RESPONSES_NORESPONSES,errorMsg)); } } @Override public ResponseInfoBean readResponseInfo(final ChaiUser theUser, final UserIdentity userIdentity, final String userGUID) throws PwmUnrecoverableException { try { if (theUser.getChaiProvider().getDirectoryVendor() != ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) { LOGGER.debug("skipping request to read NMAS responses for " + userIdentity + ", directory type is not eDirectory"); return null; } final ResponseSet responseSet = NmasCrFactory.readNmasResponseSet(theUser); if (responseSet == null) { return null; } final ResponseInfoBean responseInfoBean = CrOperators.convertToNoAnswerInfoBean(responseSet,DataStorageMethod.NMAS); responseInfoBean.setTimestamp(null); return responseInfoBean; } catch (ChaiException e) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_RESPONSES_NORESPONSES,"unexpected error reading response info " + e.getMessage())); } } public void clearResponses( final UserIdentity userIdentity, final ChaiUser theUser, final String user ) throws PwmUnrecoverableException { try { if (theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) { NmasCrFactory.clearResponseSet(theUser); LOGGER.info("cleared responses for user " + theUser.getEntryDN() + " using NMAS method "); } } catch (ChaiException e) { final String errorMsg = "error clearing responses from nmas: " + e.getMessage(); final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_CLEARING_RESPONSES, errorMsg); final PwmUnrecoverableException pwmOE = new PwmUnrecoverableException(errorInfo); pwmOE.initCause(e); throw pwmOE; } } public void writeResponses( final UserIdentity userIdentity, final ChaiUser theUser, final String userGuid, final ResponseInfoBean responseInfoBean ) throws PwmUnrecoverableException { try { if (theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY) { final NmasResponseSet nmasResponseSet = NmasCrFactory.newNmasResponseSet( responseInfoBean.getCrMap(), responseInfoBean.getLocale(), responseInfoBean.getMinRandoms(), theUser, responseInfoBean.getCsIdentifier() ); NmasCrFactory.writeResponseSet(nmasResponseSet); LOGGER.info("saved responses for user using NMAS method "); } } catch (ChaiException e) { final String errorMsg = "error writing responses to nmas: " + e.getMessage(); final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_WRITING_RESPONSES, errorMsg); final PwmUnrecoverableException pwmOE = new PwmUnrecoverableException(errorInfo); pwmOE.initCause(e); throw pwmOE; } } private static ChallengeSet questionsToChallengeSet(final List<String> questions) throws ChaiValidationException { if (questions == null || questions.isEmpty()) { return null; } final List<Challenge> challenges = new ArrayList<>(); for (final String question : questions) { challenges.add(new ChaiChallenge(true, question, 1, 256, true, 0, false)); } return new ChaiChallengeSet(challenges,challenges.size(), PwmConstants.DEFAULT_LOCALE,"NMAS-LDAP ChallengeResponse Set"); } private static List<String> documentToQuestions(final Document doc) throws XPathExpressionException { final XPath xpath = XPathFactory.newInstance().newXPath(); final XPathExpression challengesExpr = xpath.compile("/Challenges/Challenge/text()"); final NodeList challenges = (NodeList)challengesExpr.evaluate(doc, XPathConstants.NODESET); final List<String> res = new ArrayList<>(); for (int i = 0; i < challenges.getLength(); ++i) { final String question = challenges.item(i).getTextContent(); res.add(question); } return Collections.unmodifiableList(res); } private static Document answersToDocument(final List<String> answers) throws ParserConfigurationException, IOException, SAXException { final StringBuilder xml = new StringBuilder(); xml.append("<Responses>"); for(int i = 0; i < answers.size(); i++) { xml.append("<Response index=\"").append(i + 1).append("\">"); xml.append("<![CDATA[").append(answers.get(i)).append("]]>"); xml.append("</Response>"); } xml.append("</Responses>"); return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new InputSource(new StringReader(xml.toString()))); } public class NMASCRResponseSet implements ResponseSet, Serializable { private final PwmApplication pwmApplication; private final UserIdentity userIdentity; private final ChaiConfiguration chaiConfiguration; private ChallengeSet challengeSet; private transient NMASResponseSession ldapChallengeSession; boolean passed; private NMASCRResponseSet(final PwmApplication pwmApplication, final UserIdentity userIdentity) throws Exception { this.pwmApplication = pwmApplication; this.userIdentity = userIdentity; final LdapProfile ldapProfile = pwmApplication.getConfig().getLdapProfiles().get(userIdentity.getLdapProfileID()); final Configuration config = pwmApplication.getConfig(); final List<String> ldapURLs = ldapProfile.readSettingAsStringArray(PwmSetting.LDAP_SERVER_URLS); final String proxyDN = ldapProfile.readSettingAsString(PwmSetting.LDAP_PROXY_USER_DN); final PasswordData proxyPW = ldapProfile.readSettingAsPassword(PwmSetting.LDAP_PROXY_USER_PASSWORD); chaiConfiguration = LdapOperationsHelper.createChaiConfiguration(config, ldapProfile, ldapURLs, proxyDN, proxyPW); chaiConfiguration.setSetting(ChaiSetting.PROVIDER_IMPLEMENTATION, JLDAPProviderImpl.class.getName()); cycle(); } private void cycle() throws Exception { if (ldapChallengeSession != null) { ldapChallengeSession.close(); ldapChallengeSession = null; } final LDAPConnection ldapConnection = makeLdapConnection(); ldapChallengeSession = new NMASResponseSession(userIdentity.getUserDN(),ldapConnection); final List<String> questions = ldapChallengeSession.getQuestions(); challengeSet = questionsToChallengeSet(questions); } private LDAPConnection makeLdapConnection() throws Exception { final ChaiProvider chaiProvider = ChaiProviderFactory.createProvider(chaiConfiguration); final ChaiUser theUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), chaiProvider); try { if (theUser.isPasswordLocked()) { LOGGER.trace("user " + theUser.getEntryDN() + " appears to be intruder locked, aborting nmas ResponseSet loading" ); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_INTRUDER_LDAP,"nmas account is intruder locked-out")); } else if (!theUser.isAccountEnabled()) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_RESPONSES_NORESPONSES,"nmas account is disabled")); } } catch (ChaiException e) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_RESPONSES_NORESPONSES,"unable to evaluate nmas account status attributes")); } return (LDAPConnection)((ChaiProviderImplementor)chaiProvider).getConnectionObject(); } public ChallengeSet getChallengeSet() throws ChaiValidationException { if (passed) { throw new IllegalStateException("validation has already been passed"); } return challengeSet; } public ChallengeSet getPresentableChallengeSet() throws ChaiValidationException { return getChallengeSet(); } public boolean meetsChallengeSetRequirements(final ChallengeSet challengeSet) throws ChaiValidationException { if (challengeSet.getRequiredChallenges().size() > this.getChallengeSet().getRequiredChallenges().size()) { LOGGER.debug("failed meetsChallengeSetRequirements, not enough required challenge"); return false; } for (final Challenge loopChallenge : challengeSet.getRequiredChallenges()) { if (loopChallenge.isAdminDefined()) { if (!this.getChallengeSet().getChallengeTexts().contains(loopChallenge.getChallengeText())) { LOGGER.debug("failed meetsChallengeSetRequirements, missing required challenge text: '" + loopChallenge.getChallengeText() + "'"); return false; } } } if (challengeSet.getMinRandomRequired() > 0) { if (this.getChallengeSet().getChallenges().size() < challengeSet.getMinRandomRequired()) { LOGGER.debug("failed meetsChallengeSetRequirements, not enough questions to meet minrandom; minRandomRequired=" + challengeSet.getMinRandomRequired() + ", ChallengesInSet=" + this.getChallengeSet().getChallenges().size()); return false; } } return true; } public String stringValue() throws UnsupportedOperationException, ChaiOperationException { throw new UnsupportedOperationException("not supported"); } public boolean test(final Map<Challenge, String> challengeStringMap) throws ChaiUnavailableException { if (passed) { throw new IllegalStateException("test may not be called after success returned"); } final List<String> answers = new ArrayList<>(challengeStringMap == null ? Collections.<String>emptyList() : challengeStringMap.values()); if (answers.isEmpty() || answers.size() < challengeSet.minimumResponses()) { return false; } for (final String answer : answers) { if (answer == null || answer.length() < 1) { return false; } } try { passed = ldapChallengeSession.testAnswers(answers); } catch (Exception e) { LOGGER.error("error testing responses: " + e.getMessage()); } if (!passed) { try { cycle(); pwmApplication.getIntruderManager().convenience().checkUserIdentity(userIdentity); if (challengeSet == null) { final String errorMsg = "unable to load next challenge set"; throw new ChaiUnavailableException(errorMsg, ChaiError.UNKNOWN); } } catch (PwmException e) { final String errorMsg = "error reading next challenges after testing responses: " + e.getMessage(); LOGGER.error("error reading next challenges after testing responses: " + e.getMessage()); final ChaiUnavailableException chaiUnavailableException = new ChaiUnavailableException(errorMsg,ChaiError.UNKNOWN); chaiUnavailableException.initCause(e); throw chaiUnavailableException; } catch (Exception e) { final String errorMsg = "error reading next challenges after testing responses: " + e.getMessage(); LOGGER.error("error reading next challenges after testing responses: " + e.getMessage()); throw new ChaiUnavailableException(errorMsg,ChaiError.UNKNOWN); } } else { ldapChallengeSession.close(); } return passed; } @Override public Locale getLocale() throws ChaiUnavailableException, IllegalStateException, ChaiOperationException { return PwmConstants.DEFAULT_LOCALE; } @Override public Date getTimestamp() throws ChaiUnavailableException, IllegalStateException, ChaiOperationException { return null; } @Override public Map<Challenge, String> getHelpdeskResponses() { return Collections.emptyMap(); } @Override public List<ChallengeBean> asChallengeBeans(final boolean b) { return Collections.emptyList(); } @Override public List<ChallengeBean> asHelpdeskChallengeBeans(final boolean b) { return Collections.emptyList(); } } private class NMASResponseSession { private LDAPConnection ldapConnection; private final GenLcmUI lcmEnv; private NMASSessionThread nmasSessionThread; private boolean completeOnUnsupportedFailure = false; NMASResponseSession(final String userDN, final LDAPConnection ldapConnection) throws LCMRegistryException, PwmUnrecoverableException { this.ldapConnection = ldapConnection; lcmEnv = new GenLcmUI(); final GenLCMRegistry lcmRegistry = new GenLCMRegistry(); lcmRegistry.registerLcm("com.novell.security.nmas.lcm.chalresp.XmlChalRespLCM"); nmasSessionThread = new NMASSessionThread(this); final ChalRespCallbackHandler cbh = new ChalRespCallbackHandler(lcmEnv, lcmRegistry); nmasSessionThread.startLogin(userDN, ldapConnection, cbh); } public List<String> getQuestions() throws XPathExpressionException { final LCMUserPrompt prompt = lcmEnv.getNextUserPrompt(); if (prompt == null) { return null; } final Document doc = prompt.getLcmXmlDataDoc(); return documentToQuestions(doc); } public boolean testAnswers(final List<String> answers) throws SAXException, IOException, ParserConfigurationException, PwmUnrecoverableException { if (nmasSessionThread.getLoginState() == NMASThreadState.ABORTED) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE,"nmas ldap connection has been disconnected or timed out")); } final Document doc = answersToDocument(answers); lcmEnv.setUserResponse(new LCMUserResponse(doc)); final com.novell.security.nmas.client.NMASLoginResult loginResult = nmasSessionThread.getLoginResult(); final boolean result = loginResult.getNmasRetCode() == 0; if (result) { ldapConnection = loginResult.getLdapConnection(); } return result; } public void close() { if (this.ldapConnection != null) { try { this.ldapConnection.disconnect(); } catch (LDAPException e) { LOGGER.error("error closing ldap connection: " + e.getMessage(), e); } this.ldapConnection = null; } } private boolean unsupportedCallbackHasOccurred = false; private class ChalRespCallbackHandler extends com.novell.security.nmas.client.NMASCallbackHandler { ChalRespCallbackHandler(final LCMEnvironment lcmenvironment, final LCMRegistry lcmregistry) { super(lcmenvironment, lcmregistry); } public void handle(final Callback[] callbacks) throws UnsupportedCallbackException { LOGGER.trace("entering ChalRespCallbackHandler.handle()"); for (final Callback callback : callbacks) { final String callbackClassname = callback.getClass().getName(); LOGGER.trace("evaluating callback: " + callback.toString() + ", class=" + callbackClassname); // note in some cases instanceof check fails due to classloader issues, using getName string comparison instead if (NMASCompletionCallback.class.getName().equals(callbackClassname)) { LOGGER.trace("received NMASCompletionCallback, ignoring"); } else if (NMASCallback.class.getName().equals(callbackClassname)) { LOGGER.trace("callback is instance of NMASCompletionCallback, calling handleNMASCallback()"); try { handleNMASCallback((NMASCallback) callback); } catch (com.novell.security.nmas.client.InvalidNMASCallbackException e) { LOGGER.error("error processing NMASCallback: " + e.getMessage(),e); } } else if (LCMUserPromptCallback.class.getName().equals(callbackClassname)) { LOGGER.trace("callback is instance of LCMUserPromptCallback, calling handleLCMUserPromptCallback()"); try { handleLCMUserPromptCallback((LCMUserPromptCallback) callback); } catch (LCMUserPromptException e) { LOGGER.error("error processing LCMUserPromptCallback: " + e.getMessage(),e); } } else { unsupportedCallbackHasOccurred = true; LOGGER.trace("throwing UnsupportedCallbackException for " + callback.toString() + ", class=" + callback.getClass().getName()); throw new UnsupportedCallbackException(callback); } } } public int awaitRetCode() { final Instant startTime = Instant.now(); boolean done = this.isNmasDone(); Date lastLogTime = new Date(); while (!done && TimeDuration.fromCurrent(startTime).isShorterThan(maxThreadIdleTime)) { LOGGER.trace("attempt to read return code, but isNmasDone=false, will await completion"); JavaHelper.pause(10); if (completeOnUnsupportedFailure) { done = unsupportedCallbackHasOccurred || this.isNmasDone(); } else { done = this.isNmasDone(); } if (TimeDuration.SECOND.isLongerThan(TimeDuration.fromCurrent(lastLogTime))) { LOGGER.trace("waiting for return code: " + TimeDuration.fromCurrent(startTime).asCompactString() + " unsupportedCallbackHasOccurred=" + unsupportedCallbackHasOccurred); lastLogTime = new Date(); } } LOGGER.debug("read return code in " + TimeDuration.fromCurrent(startTime).asCompactString()); return this.getNmasRetCode(); } } } private enum NMASThreadState { NEW, BIND, COMPLETED, ABORTED, } private class NMASSessionThread extends Thread { private volatile Date lastActivityTimestamp = new Date(); private volatile NMASThreadState loginState = NMASThreadState.NEW; private volatile boolean loginResultReady = false; private volatile com.novell.security.nmas.client.NMASLoginResult loginResult = null; private volatile NMASResponseSession.ChalRespCallbackHandler callbackHandler = null; private volatile LDAPConnection ldapConn = null; private volatile String loginDN = null; private final NMASResponseSession nmasResponseSession; private final int threadID; NMASSessionThread(final NMASResponseSession nmasResponseSession) { this.nmasResponseSession = nmasResponseSession; this.threadID = threadCounter++; setLoginState(NMASThreadState.NEW); } private void setLoginState(final NMASThreadState paramInt) { this.loginState = paramInt; } public NMASThreadState getLoginState() { return this.loginState; } public Date getLastActivityTimestamp() { return lastActivityTimestamp; } private synchronized void setLoginResult(final com.novell.security.nmas.client.NMASLoginResult paramNMASLoginResult) { this.loginResult = paramNMASLoginResult; this.loginResultReady = true; this.lastActivityTimestamp = new Date(); } public final synchronized com.novell.security.nmas.client.NMASLoginResult getLoginResult() { while (!this.loginResultReady) { try { wait(); } catch (Exception localException) { /* noop */ } } lastActivityTimestamp = new Date(); return this.loginResult; } public void startLogin( final String userDN, final LDAPConnection ldapConnection, final NMASResponseSession.ChalRespCallbackHandler paramCallbackHandler ) throws PwmUnrecoverableException { if (sessionMonitorThreads.size() > maxThreadCount) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TOO_MANY_THREADS,"NMASSessionMonitor maximum thread count (" + maxThreadCount + ") exceeded")); } this.loginDN = userDN; this.ldapConn = ldapConnection; this.callbackHandler = paramCallbackHandler; this.loginResultReady = false; setLoginState(NMASThreadState.NEW); setDaemon(true); setName(PwmConstants.PWM_APP_NAME + "-NMASSessionThread thread id=" + threadID); lastActivityTimestamp = new Date(); start(); } public void run() { try { LOGGER.trace("starting NMASSessionThread, activeCount=" + sessionMonitorThreads.size() + ", " + this.toDebugString()); sessionMonitorThreads.add(this); controlWatchdogThread(); doLoginSequence(); } finally { sessionMonitorThreads.remove(this); controlWatchdogThread(); LOGGER.trace("exiting NMASSessionThread, activeCount=" + sessionMonitorThreads.size() + ", " + this.toDebugString()); } } private void doLoginSequence() { if (loginState == NMASThreadState.ABORTED || loginState == NMASThreadState.COMPLETED) { return; } lastActivityTimestamp = new Date(); if (this.ldapConn == null) { setLoginState(NMASThreadState.COMPLETED); setLoginResult(new com.novell.security.nmas.client.NMASLoginResult(-1681)); lastActivityTimestamp = new Date(); return; } try { setLoginState(NMASThreadState.BIND); lastActivityTimestamp = new Date(); try { this.ldapConn.bind( this.loginDN, "dn:" + this.loginDN, new String[] { "NMAS_LOGIN" }, new HashMap<>(CR_OPTIONS_MAP), this.callbackHandler ); } catch (NullPointerException e) { LOGGER.error("NullPointer error during CallBackHandler-NMASCR-bind; this is usually the result of an ldap disconnection, thread=" + this.toDebugString()); this.setLoginState(NMASThreadState.ABORTED); return; } if (loginState == NMASThreadState.ABORTED) { return; } setLoginState(NMASThreadState.COMPLETED); lastActivityTimestamp = new Date(); setLoginResult(new com.novell.security.nmas.client.NMASLoginResult(this.callbackHandler.awaitRetCode(), this.ldapConn)); lastActivityTimestamp = new Date(); } catch (LDAPException e) { if (loginState == NMASThreadState.ABORTED) { return; } final String ldapErrorMessage = e.getLDAPErrorMessage(); if (ldapErrorMessage != null) { LOGGER.error("NMASLoginMonitor: LDAP error (" + ldapErrorMessage + ")"); } else { LOGGER.error("NMASLoginMonitor: LDAPException " + e.toString()); } setLoginState(NMASThreadState.COMPLETED); final com.novell.security.nmas.client.NMASLoginResult localNMASLoginResult = new com.novell.security.nmas.client.NMASLoginResult(this.callbackHandler.awaitRetCode(), e); setLoginResult(localNMASLoginResult); } lastActivityTimestamp = new Date(); } public void abort() { setLoginState(NMASThreadState.ABORTED); setLoginResult(new com.novell.security.nmas.client.NMASLoginResult(-1681)); try { this.notify(); } catch (Exception e) { /* ignore */ } try { this.nmasResponseSession.lcmEnv.setUserResponse(null); } catch (Exception e) { LOGGER.trace("error during NMASResponseSession abort: " + e.getMessage(),e); } } public String toDebugString() { final TreeMap<String,String> debugInfo = new TreeMap<>(); debugInfo.put("loginDN", this.loginDN); debugInfo.put("id",Integer.toString(threadID)); debugInfo.put("loginState", this.getLoginState().toString()); debugInfo.put("loginResultReady",Boolean.toString(this.loginResultReady)); debugInfo.put("idleTime", TimeDuration.fromCurrent(this.getLastActivityTimestamp()).asCompactString()); return "NMASSessionThread: " + JsonUtil.serialize(debugInfo); } } private class ThreadWatchdogTask extends TimerTask { private final boolean debugOutput; ThreadWatchdogTask(final boolean debugOutput) { this.debugOutput = debugOutput; } @Override public void run() { if (debugOutput) { logThreadInfo(); } final List<NMASSessionThread> threads = new ArrayList<>(sessionMonitorThreads); for (final NMASSessionThread thread : threads) { final TimeDuration idleTime = TimeDuration.fromCurrent(thread.getLastActivityTimestamp()); if (idleTime.isLongerThan(maxThreadIdleTime)) { LOGGER.debug("killing thread due to inactivity " + thread.toDebugString()); thread.abort(); } } } private void logThreadInfo() { final List<NMASSessionThread> threads = new ArrayList<>(sessionMonitorThreads); final StringBuilder threadDebugInfo = new StringBuilder(); threadDebugInfo.append("NMASCrOperator watchdog timer, activeCount=").append(threads.size()); threadDebugInfo.append(", maxIdleThreadTime=").append(maxThreadIdleTime.asCompactString()); for (final NMASSessionThread thread : threads) { threadDebugInfo.append("\n ").append(thread.toDebugString()); } LOGGER.trace(threadDebugInfo.toString()); } } /** * This SASL Provider is a replacement for ldap.jar!/com/novell/sasl/client/NovellSaslProvider.class. The primary * difference is that it registers <code>{@link NMASCrPwmSaslFactory}</code> as the factory instead of * ldap-2013.04.26.jar!/com/novell/sasl/client/ClientFactory.class */ public static class NMASCrPwmSaslProvider extends Provider { private static final PwmLogger LOGGER = PwmLogger.forClass(NMASCrPwmSaslProvider.class); public static final String SASL_PROVIDER_NAME = "NMAS_LOGIN"; private static final String INFO = "PWM NMAS Sasl Provider"; public NMASCrPwmSaslProvider() { super("SaslClientFactory", 1.1, INFO); final NMASCrPwmSaslProvider thisInstance = NMASCrPwmSaslProvider.this; AccessController.doPrivileged(new PrivilegedAction() { public Object run() { try { final String saslFactoryName = password.pwm.util.operations.cr.NMASCrOperator.NMASCrPwmSaslFactory.class.getName(); thisInstance.put("SaslClientFactory." + SASL_PROVIDER_NAME, saslFactoryName); } catch (SecurityException e) { LOGGER.warn("error registering " + NMASCrPwmSaslProvider.class.getSimpleName() + " SASL Provider, error: " + e.getMessage(), e); } return null; } }); } } /** * This SASL Client Factory is a replacement for ldap.jar!/com/novell/sasl/client/ClientFactory.class. It's only difference with * that class is that it uses a threadlocal classloader to load a backing NMAS Sasl Client. The default factory uses a static reference * to create a new SaslClient, which causes issues with tomcat and multiple classloaders. */ public static class NMASCrPwmSaslFactory implements SaslClientFactory { private static final PwmLogger LOGGER = PwmLogger.forClass(NMASCrPwmSaslFactory.class); public NMASCrPwmSaslFactory() { LOGGER.debug("initializing NMASCrPwmSaslFactory instance"); } @Override public SaslClient createSaslClient(final String[] mechanisms, final String authorizationId, final String protocol, final String serverName, final Map<String, ?> props, final CallbackHandler cbh) throws SaslException { try { LOGGER.trace("creating new SASL Client instance"); final SaslClientFactory realFactory = getRealSaslClientFactory(); return realFactory.createSaslClient(mechanisms, authorizationId, protocol, serverName, props, cbh); } catch (Throwable t) { LOGGER.error("error creating backing sasl factory: " + t.getMessage(), t); } return null; } private SaslClientFactory getRealSaslClientFactory() throws IllegalAccessException, InstantiationException, ClassNotFoundException { final String className = "com.novell.sasl.client.ClientFactory"; final ClassLoader threadLocalClassLoader = Thread.currentThread().getContextClassLoader(); final Class threadLocalClass = threadLocalClassLoader.loadClass(className); return (SaslClientFactory) threadLocalClass.newInstance(); } @Override public String[] getMechanismNames(final Map<String, ?> props) { try { final SaslClientFactory realFactory = getRealSaslClientFactory(); return realFactory.getMechanismNames(props); } catch (Throwable t) { LOGGER.error("error creating backing sasl factory: " + t.getMessage(), t); } return new String[0]; } } }