From 9441fa37c529e94b2432bcc3e1ef8baef71f2810 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 8 Jan 2016 15:49:16 +0100 Subject: [PATCH] validate usernames via Authenticator.validate_username base class configurable with Authenticator.username_pattern --- jupyterhub/apihandlers/users.py | 11 +++++++++++ jupyterhub/app.py | 6 ++++++ jupyterhub/auth.py | 26 +++++++++++++++++++++++++- jupyterhub/tests/test_api.py | 11 +++++++++++ jupyterhub/tests/test_auth.py | 9 +++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 7c2fd151..e27d0cdc 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -33,14 +33,25 @@ class UserListAPIHandler(APIHandler): admin = data.get('admin', False) to_create = [] + invalid_names = [] for name in usernames: name = self.authenticator.normalize_username(name) + if not self.authenticator.validate_username(name): + invalid_names.append(name) + continue user = self.find_user(name) if user is not None: self.log.warn("User %s already exists" % name) else: to_create.append(name) + if invalid_names: + if len(invalid_names) == 1: + msg = "Invalid username: %s" % invalid_names[0] + else: + msg = "Invalid usernames: %s" % ', '.join(invalid_names) + raise web.HTTPError(400, msg) + if not to_create: raise web.HTTPError(400, "All %i users already exist" % len(usernames)) diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 59779577..bf321f23 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -634,6 +634,9 @@ class JupyterHub(Application): self.authenticator.normalize_username(name) for name in self.authenticator.admin_users ] + for username in admin_users: + if not self.authenticator.validate_username(username): + raise ValueError("username %r is not valid" % username) if not admin_users: self.log.warning("No admin users, admin interface will be unavailable.") @@ -658,6 +661,9 @@ class JupyterHub(Application): self.authenticator.normalize_username(name) for name in self.authenticator.whitelist ] + for username in whitelist: + if not self.authenticator.validate_username(username): + raise ValueError("username %r is not valid" % username) if not whitelist: self.log.info("Not using whitelist. Any authenticated user will be allowed.") diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 27e473ff..c81caf04 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -14,7 +14,7 @@ from tornado import gen import pamela from traitlets.config import LoggingConfigurable -from traitlets import Bool, Set, Unicode, Any +from traitlets import Bool, Set, Unicode, Dict, Any from .handlers.login import LoginHandler from .utils import url_path_join @@ -53,6 +53,28 @@ class Authenticator(LoggingConfigurable): """ ) + username_pattern = Unicode(config=True, + help="""Regular expression pattern for validating usernames. + + If not defined: allow any username. + """ + ) + def _username_pattern_changed(self, name, old, new): + if not new: + self.username_regex = None + self.username_regex = re.compile(new) + + username_regex = Any() + + def validate_username(self, username): + """Validate a (normalized) username. + + Return True if username is valid, False otherwise. + """ + if not self.username_regex: + return True + return bool(self.username_regex.match(username)) + username_map = Dict(config=True, help="""Dictionary mapping authenticator usernames to JupyterHub users. @@ -144,6 +166,8 @@ class Authenticator(LoggingConfigurable): Subclasses may do more extensive things, such as adding actual unix users. """ + if not self.validate_username(user.name): + raise ValueError("Invalid username: %s" % user.name) if self.whitelist: self.whitelist.add(user.name) diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index facbb2c7..413ff913 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -209,6 +209,17 @@ def test_add_multi_user_bad(app): r = api_request(app, 'users', method='post', data='[]') assert r.status_code == 400 + +def test_add_multi_user_invalid(app): + app.authenticator.username_pattern = r'w.*' + r = api_request(app, 'users', method='post', + data=json.dumps({'usernames': ['Willow', 'Andrew', 'Tara']}) + ) + app.authenticator.username_pattern = '' + assert r.status_code == 400 + assert r.json()['message'] == 'Invalid usernames: andrew, tara' + + def test_add_multi_user(app): db = app.db names = ['a', 'b'] diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index 365c72a3..5907244a 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -169,3 +169,12 @@ def test_username_map(io_loop): assert authorized == 'inara' +def test_validate_names(io_loop): + a = auth.PAMAuthenticator() + assert a.validate_username('willow') + assert a.validate_username('giles') + a = auth.PAMAuthenticator(username_pattern='w.*') + assert not a.validate_username('xander') + assert a.validate_username('willow') + +