Allow and enable PAM account stack checking

JH can now differentiate between authenticated and authorized users via PAM
This allows JH to respect PAM-accessible user access controls.

This also fixes missing PAMAuthenticator.encoding usages.
This commit is contained in:
Unknown
2018-03-17 18:53:00 -05:00
committed by Will Starms
parent e81eb9a5f8
commit 25373f510d
3 changed files with 60 additions and 6 deletions

View File

@@ -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.")

View File

@@ -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:

View File

@@ -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():