From 1a29328d06160caa5e08b46e6cad2eba6ecd9ba3 Mon Sep 17 00:00:00 2001 From: MinRK Date: Tue, 23 Sep 2014 16:09:26 -0700 Subject: [PATCH] Add Authenticator.add_user hook and .delete_user This hook can be used to trigger events, such as user validation, or creating of system users. Adds a LocalAuthenticator class that implements checking for and rudimentary creation of system users. --- jupyterhub/apihandlers/users.py | 13 +++-- jupyterhub/auth.py | 84 +++++++++++++++++++++++++++++++-- jupyterhub/tests/mocking.py | 6 ++- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 0353e58c..50c37965 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -85,10 +85,11 @@ class UserAPIHandler(BaseUserHandler): user.admin = data['admin'] self.db.commit() - # add to whitelist, if a whitelist is in use - - if self.authenticator and self.authenticator.whitelist: - self.authenticator.whitelist.add(user.name) + try: + self.authenticator.add_user(user) + except Exception: + self.db.delete(user) + self.db.commit() self.write(json.dumps(self.user_model(user))) self.set_status(201) @@ -104,9 +105,7 @@ class UserAPIHandler(BaseUserHandler): if user.spawner is not None: yield self.stop_single_user(user) - # remove the user from the whitelist, if there is one - if self.authenticator and user.name in self.authenticator.whitelist: - self.authenticator.whitelist.remove(user.name) + self.authenticator.delete_user(user) # remove from the db self.db.delete(user) diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index dbea89bb..93d084d4 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -3,11 +3,14 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +import pwd +from subprocess import check_call, check_output, CalledProcessError + from tornado import gen import simplepam from IPython.config import LoggingConfigurable -from IPython.utils.traitlets import Unicode, Set +from IPython.utils.traitlets import Bool, Set, Unicode from .utils import url_path_join @@ -35,6 +38,25 @@ class Authenticator(LoggingConfigurable): and return None on failed authentication. """ + def add_user(self, user): + """Add a new user + + By default, this just adds the user to the whitelist. + + Subclasses may do more extensive things, + such as adding actual unix users. + """ + if self.whitelist: + self.whitelist.add(user.name) + + def delete_user(self, user): + """Triggered when a user is deleted. + + Removes the user from the whitelist. + """ + if user.name in self.whitelist: + self.whitelist.remove(user.name) + def login_url(self, base_url): """Override to register a custom login handler""" return url_path_join(base_url, 'login') @@ -50,10 +72,66 @@ class Authenticator(LoggingConfigurable): """ return [] +class LocalAuthenticator(Authenticator): + """Base class for Authenticators that work with local *ix users + + Checks for local users, and can attempt to create them if they exist. + """ + + create_system_users = Bool(False, config=True, + help="""If a user is added that doesn't exist on the system, + should I try to create the system user? + """ + ) + + def add_user(self, user): + """Add a new user + + By default, this just adds the user to the whitelist. + + Subclasses may do more extensive things, + such as adding actual unix users. + """ + if not self.system_user_exists(user): + if self.create_system_users: + self.add_system_user(user) + else: + raise KeyError("User %s does not exist." % user.name) + + super(LocalAuthenticator, self).add_user(user) + + def system_user_exists(self, user): + """Check if the user exists on the system""" + try: + pwd.getpwnam(user.name) + except KeyError: + return False + else: + return True + + def add_system_user(user): + """Create a new *ix user on the system. Works on FreeBSD and Linux, at least.""" + name = user.name + for useradd in ( + ['pw', 'useradd', '-m'], + ['useradd', '-m'], + ): + try: + check_output(['which', useradd[0]]) + except CalledProcessError: + continue + else: + break + else: + raise RuntimeError("I don't know how to add users on this system.") + + check_call(useradd + [name]) -class PAMAuthenticator(Authenticator): + +class PAMAuthenticator(LocalAuthenticator): + """Authenticate local *ix users with PAM""" encoding = Unicode('utf8', config=True, - help="""The encoding to use for PAM """ + help="""The encoding to use for PAM""" ) service = Unicode('login', config=True, help="""The PAM service to use for authentication.""" diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index c82d3560..d576481b 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -14,7 +14,7 @@ from IPython.utils.py3compat import unicode_type from ..spawner import LocalProcessSpawner from ..app import JupyterHubApp -from ..auth import PAMAuthenticator +from ..auth import PAMAuthenticator, Authenticator from .. import orm def mock_authenticate(username, password, service='login'): @@ -46,6 +46,10 @@ class MockSpawner(LocalProcessSpawner): class MockPAMAuthenticator(PAMAuthenticator): + def system_user_exists(self, user): + # skip the add-system-user bit + return True + def authenticate(self, *args, **kwargs): with mock.patch('simplepam.authenticate', mock_authenticate): return super(MockPAMAuthenticator, self).authenticate(*args, **kwargs)