mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 15:03:02 +00:00
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:
@@ -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.")
|
||||
|
@@ -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:
|
||||
|
@@ -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():
|
||||
|
Reference in New Issue
Block a user