From ea7b1caa4eb995b403c9f4a09bdc17d0a298224c Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 26 Apr 2018 12:54:43 +0300 Subject: [PATCH 1/2] Add blacklist to auth.py - Introduce blacklist to supplement white list - Original code by github:ndiy, updated by github:rkdarst --- jupyterhub/auth.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 2d3c5bb9..d9429bc6 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -88,6 +88,20 @@ class Authenticator(LoggingConfigurable): """ ).tag(config=True) + blacklist = Set( + help=""" + Blacklist of usernames that are not allowed to log in. + + Use this with supported authenticators to restrict which users can not log in. This is an + additional blacklist that further restricts users, beyond whatever restrictions the + authenticator has in place. + + If empty, does not perform any additional restriction. + + .. versionadded: 0.9 + """ + ).tag(config=True) + @observe('whitelist') def _check_whitelist(self, change): short_names = [name for name in change['new'] if len(name) <= 1] @@ -205,6 +219,21 @@ class Authenticator(LoggingConfigurable): return True return username in self.whitelist + def check_blacklist(self, username): + """Check if a username is blocked to authenticate based on blacklist configuration + + Return True if username is allowed, False otherwise. + No blacklist means any username is allowed. + + Names are normalized *before* being checked against the blacklist. + + .. versionadded: 0.9 + """ + if not self.blacklist: + # No blacklist means any name is allowed + return True + return username not in self.blacklist + async def get_authenticated_user(self, handler, data): """Authenticate the user who is attempting to log in @@ -244,6 +273,13 @@ class Authenticator(LoggingConfigurable): self.log.warning("Disallowing invalid username %r.", username) return + blacklist_pass = await maybe_future(self.check_blacklist(username)) + if blacklist_pass: + pass + else: + self.log.warning("User %r in blacklist. Stop authentication", username) + return + whitelist_pass = await maybe_future(self.check_whitelist(username)) if whitelist_pass: return authenticated From 95a9b976493b63b9ea77a5087510cdc0f936f41c Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 26 Apr 2018 14:56:32 +0300 Subject: [PATCH 2/2] Add tests for authentication blacklist --- jupyterhub/auth.py | 2 +- jupyterhub/tests/test_auth.py | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index d9429bc6..1058fe83 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -274,13 +274,13 @@ class Authenticator(LoggingConfigurable): return blacklist_pass = await maybe_future(self.check_blacklist(username)) + whitelist_pass = await maybe_future(self.check_whitelist(username)) if blacklist_pass: pass else: self.log.warning("User %r in blacklist. Stop authentication", username) return - whitelist_pass = await maybe_future(self.check_whitelist(username)) if whitelist_pass: return authenticated else: diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index a739c786..a4761161 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -103,6 +103,65 @@ def test_pam_auth_group_whitelist(): assert authorized is None +@pytest.mark.gen_test +def test_pam_auth_blacklist(): + # Null case compared to next case + authenticator = MockPAMAuthenticator() + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'wash', + 'password': 'wash', + }) + assert authorized['name'] == 'wash' + + # Blacklist basics + authenticator = MockPAMAuthenticator(blacklist={'wash'}) + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'wash', + 'password': 'wash', + }) + assert authorized is None + + # User in both white and blacklists: default deny. Make error someday? + authenticator = MockPAMAuthenticator(blacklist={'wash'}, whitelist={'wash', 'kaylee'}) + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'wash', + 'password': 'wash', + }) + assert authorized is None + + # User not in blacklist can log in + authenticator = MockPAMAuthenticator(blacklist={'wash'}, whitelist={'wash', 'kaylee'}) + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'kaylee', + 'password': 'kaylee', + }) + assert authorized['name'] == 'kaylee' + + # User in whitelist, blacklist irrelevent + authenticator = MockPAMAuthenticator(blacklist={'mal'}, whitelist={'wash', 'kaylee'}) + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'wash', + 'password': 'wash', + }) + assert authorized['name'] == 'wash' + + # User in neither list + authenticator = MockPAMAuthenticator(blacklist={'mal'}, whitelist={'wash', 'kaylee'}) + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'simon', + 'password': 'simon', + }) + assert authorized is None + + # blacklist == {} + authenticator = MockPAMAuthenticator(blacklist=set(), whitelist={'wash', 'kaylee'}) + authorized = yield authenticator.get_authenticated_user(None, { + 'username': 'kaylee', + 'password': 'kaylee', + }) + assert authorized['name'] == 'kaylee' + + @pytest.mark.gen_test def test_pam_auth_no_such_group(): authenticator = MockPAMAuthenticator(group_whitelist={'nosuchcrazygroup'})