diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 2d3c5bb9..1058fe83 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,7 +273,14 @@ class Authenticator(LoggingConfigurable): self.log.warning("Disallowing invalid username %r.", username) 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 + 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'})