diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 03cbc515..d873b1fb 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -550,6 +550,19 @@ class PAMAuthenticator(LocalAuthenticator): """ ).tag(config=True) + check_account = Bool(True, + help=""" + Whether to check the user's account status via PAM during authentication. + + The PAM account stack performs non-authentication based account + management. It is typically used to restrict/permit access to a + service and this step is needed to access the host's user access control. + + Disabling this can be dangerous as authenticated but unauthorized users may + be granted access and, therefore, arbitrary execution on the system. + """ + ).tag(config=True) + def __init__(self, **kwargs): if pamela is None: raise _pamela_error from None @@ -563,14 +576,24 @@ class PAMAuthenticator(LocalAuthenticator): """ username = data['username'] try: - pamela.authenticate(username, data['password'], service=self.service) + pamela.authenticate(username, data['password'], service=self.service, encoding=self.encoding) except pamela.PAMError as e: if handler is not None: self.log.warning("PAM Authentication failed (%s@%s): %s", username, handler.request.remote_ip, e) else: self.log.warning("PAM Authentication failed: %s", e) else: - return username + if not self.check_account: + return username + try: + pamela.check_account(username, service=self.service, encoding=self.encoding) + except pamela.PAMError as e: + if handler is not None: + self.log.warning("PAM Account Check failed (%s@%s): %s", username, handler.request.remote_ip, e) + else: + self.log.warning("PAM Account Check failed: %s", e) + else: + return username @run_on_executor def pre_spawn_start(self, user, spawner): @@ -578,7 +601,7 @@ class PAMAuthenticator(LocalAuthenticator): if not self.open_sessions: return try: - pamela.open_session(user.name, service=self.service) + pamela.open_session(user.name, service=self.service, encoding=self.encoding) except pamela.PAMError as e: self.log.warning("Failed to open PAM session for %s: %s", user.name, e) self.log.warning("Disabling PAM sessions from now on.") @@ -590,7 +613,7 @@ class PAMAuthenticator(LocalAuthenticator): if not self.open_sessions: return try: - pamela.close_session(user.name, service=self.service) + pamela.close_session(user.name, service=self.service, encoding=self.encoding) except pamela.PAMError as e: self.log.warning("Failed to close PAM session for %s: %s", user.name, e) self.log.warning("Disabling PAM sessions from now on.") diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index bce3deec..cf49e849 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -26,7 +26,7 @@ from .utils import async_requests from pamela import PAMError -def mock_authenticate(username, password, service='login'): +def mock_authenticate(username, password, service, encoding): # just use equality for testing if password == username: return True @@ -34,7 +34,14 @@ def mock_authenticate(username, password, service='login'): raise PAMError("Fake") -def mock_open_session(username, service): +def mock_check_account(username, service, encoding): + if username.startswith('notallowed'): + raise PAMError("Fake") + else: + return True + + +def mock_open_session(username, service, encoding): pass @@ -156,6 +163,7 @@ class MockPAMAuthenticator(PAMAuthenticator): authenticate=mock_authenticate, open_session=mock_open_session, close_session=mock_open_session, + check_account=mock_check_account, ): username = yield super(MockPAMAuthenticator, self).authenticate(*args, **kwargs) if username is None: diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index 161ea16f..33aa826e 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -29,6 +29,29 @@ def test_pam_auth(): }) assert authorized is None + # Account check is on by default for increased security + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'notallowedmatch', + 'password': 'notallowedmatch', + }) + assert authorized is None + + +@pytest.mark.gen_test +def test_pam_auth_account_check_disabled(): + authenticator = MockPAMAuthenticator(check_account=False) + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'allowedmatch', + 'password': 'allowedmatch', + }) + assert authorized['name'] == 'allowedmatch' + + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'notallowedmatch', + 'password': 'notallowedmatch', + }) + assert authorized['name'] == 'notallowedmatch' + @pytest.mark.gen_test def test_pam_auth_whitelist():