mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33: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)
|
).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):
|
def __init__(self, **kwargs):
|
||||||
if pamela is None:
|
if pamela is None:
|
||||||
raise _pamela_error from None
|
raise _pamela_error from None
|
||||||
@@ -563,14 +576,24 @@ class PAMAuthenticator(LocalAuthenticator):
|
|||||||
"""
|
"""
|
||||||
username = data['username']
|
username = data['username']
|
||||||
try:
|
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:
|
except pamela.PAMError as e:
|
||||||
if handler is not None:
|
if handler is not None:
|
||||||
self.log.warning("PAM Authentication failed (%s@%s): %s", username, handler.request.remote_ip, e)
|
self.log.warning("PAM Authentication failed (%s@%s): %s", username, handler.request.remote_ip, e)
|
||||||
else:
|
else:
|
||||||
self.log.warning("PAM Authentication failed: %s", e)
|
self.log.warning("PAM Authentication failed: %s", e)
|
||||||
else:
|
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
|
@run_on_executor
|
||||||
def pre_spawn_start(self, user, spawner):
|
def pre_spawn_start(self, user, spawner):
|
||||||
@@ -578,7 +601,7 @@ class PAMAuthenticator(LocalAuthenticator):
|
|||||||
if not self.open_sessions:
|
if not self.open_sessions:
|
||||||
return
|
return
|
||||||
try:
|
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:
|
except pamela.PAMError as e:
|
||||||
self.log.warning("Failed to open PAM session for %s: %s", user.name, e)
|
self.log.warning("Failed to open PAM session for %s: %s", user.name, e)
|
||||||
self.log.warning("Disabling PAM sessions from now on.")
|
self.log.warning("Disabling PAM sessions from now on.")
|
||||||
@@ -590,7 +613,7 @@ class PAMAuthenticator(LocalAuthenticator):
|
|||||||
if not self.open_sessions:
|
if not self.open_sessions:
|
||||||
return
|
return
|
||||||
try:
|
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:
|
except pamela.PAMError as e:
|
||||||
self.log.warning("Failed to close PAM session for %s: %s", user.name, e)
|
self.log.warning("Failed to close PAM session for %s: %s", user.name, e)
|
||||||
self.log.warning("Disabling PAM sessions from now on.")
|
self.log.warning("Disabling PAM sessions from now on.")
|
||||||
|
@@ -26,7 +26,7 @@ from .utils import async_requests
|
|||||||
|
|
||||||
from pamela import PAMError
|
from pamela import PAMError
|
||||||
|
|
||||||
def mock_authenticate(username, password, service='login'):
|
def mock_authenticate(username, password, service, encoding):
|
||||||
# just use equality for testing
|
# just use equality for testing
|
||||||
if password == username:
|
if password == username:
|
||||||
return True
|
return True
|
||||||
@@ -34,7 +34,14 @@ def mock_authenticate(username, password, service='login'):
|
|||||||
raise PAMError("Fake")
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -156,6 +163,7 @@ class MockPAMAuthenticator(PAMAuthenticator):
|
|||||||
authenticate=mock_authenticate,
|
authenticate=mock_authenticate,
|
||||||
open_session=mock_open_session,
|
open_session=mock_open_session,
|
||||||
close_session=mock_open_session,
|
close_session=mock_open_session,
|
||||||
|
check_account=mock_check_account,
|
||||||
):
|
):
|
||||||
username = yield super(MockPAMAuthenticator, self).authenticate(*args, **kwargs)
|
username = yield super(MockPAMAuthenticator, self).authenticate(*args, **kwargs)
|
||||||
if username is None:
|
if username is None:
|
||||||
|
@@ -29,6 +29,29 @@ def test_pam_auth():
|
|||||||
})
|
})
|
||||||
assert authorized is None
|
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
|
@pytest.mark.gen_test
|
||||||
def test_pam_auth_whitelist():
|
def test_pam_auth_whitelist():
|
||||||
|
Reference in New Issue
Block a user