Added authenticator hook for synchronizing user groups

- Added hook function stub to authenticator base class
- Added new config option `manage_groups` to base `Authenticator` class
- Call authenticator hook from `refresh_auth`-function in `Base` handler class
- Added example
This commit is contained in:
Thomas Li Fredriksen
2021-07-23 14:32:44 +02:00
committed by Min RK
parent dcf21d53fd
commit 144abcb965
7 changed files with 88 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
"""sample jupyterhub config file for testing
configures jupyterhub with dummyauthenticator and simplespawner
to enable testing without administrative privileges.
"""
c = get_config() # noqa
c.Application.log_level = 'DEBUG'
from oauthenticator.azuread import AzureAdOAuthenticator
import os
c.JupyterHub.authenticator_class = AzureAdOAuthenticator
c.AzureAdOAuthenticator.client_id = os.getenv("AAD_CLIENT_ID")
c.AzureAdOAuthenticator.client_secret = os.getenv("AAD_CLIENT_SECRET")
c.AzureAdOAuthenticator.oauth_callback_url = os.getenv("AAD_CALLBACK_URL")
c.AzureAdOAuthenticator.tenant_id = os.getenv("AAD_TENANT_ID")
c.AzureAdOAuthenticator.username_claim = "email"
c.AzureAdOAuthenticator.authorize_url = os.getenv("AAD_AUTHORIZE_URL")
c.AzureAdOAuthenticator.token_url = os.getenv("AAD_TOKEN_URL")
c.Authenticator.authenticator_managed_groups = True
c.Authenticator.refresh_pre_spawn = True
# Optionally set a global password that all users must use
# c.DummyAuthenticator.password = "your_password"
from jupyterhub.spawner import SimpleLocalProcessSpawner
c.JupyterHub.spawner_class = SimpleLocalProcessSpawner

View File

@@ -0,0 +1,2 @@
oauthenticator
pyjwt

View File

@@ -68,6 +68,10 @@ class GroupListAPIHandler(_GroupAPIHandler):
@needs_scope('admin:groups')
async def post(self):
"""POST creates Multiple groups"""
if self.authenticator.manage_groups:
raise web.HTTPError(400, "Group management via API is disabled")
model = self.get_json_body()
if not model or not isinstance(model, dict) or not model.get('groups'):
raise web.HTTPError(400, "Must specify at least one group to create")

View File

@@ -2001,6 +2001,9 @@ class JupyterHub(Application):
async def init_groups(self):
"""Load predefined groups into the database"""
db = self.db
if self.authenticator.manage_groups and self.load_groups:
raise ValueError("Group management has been offloaded to the authenticator")
for name, usernames in self.load_groups.items():
group = orm.Group.find(db, name)
if group is None:

View File

@@ -635,6 +635,30 @@ class Authenticator(LoggingConfigurable):
"""
self.allowed_users.discard(user.name)
manage_groups = Bool(
False,
config=True,
help="""Let authenticator manage user groups
Authenticator must implement get_user_groups for this to be useful.
""",
)
def load_user_groups(self, user, auth_state):
"""Hook called allowing authenticator to read user groups
Updates user group memberships
Args:
auth_state (dict): Proprietary dict returned by authenticator
user(User): the User object associated with the auth-state
Returns:
groups (list):
List of user group memberships
"""
return None
auto_login = Bool(
False,
config=True,

View File

@@ -622,6 +622,9 @@ class BaseHandler(RequestHandler):
def authenticate(self, data):
return maybe_future(self.authenticator.get_authenticated_user(self, data))
def load_user_groups(self, user, auth_info):
return maybe_future(self.authenticator.load_user_groups(user, auth_info))
def get_next_url(self, user=None, default=None):
"""Get the next_url for login redirect
@@ -779,7 +782,15 @@ class BaseHandler(RequestHandler):
if not self.authenticator.enable_auth_state:
# auth_state is not enabled. Force None.
auth_state = None
if self.authenticator.manage_groups:
# Run authenticator user-group reload hook
user_groups = await self.load_user_groups(user, authenticated)
if user_groups is not None:
user.sync_groups(user_groups)
await user.save_auth_state(auth_state)
return user
async def login_user(self, data=None):
@@ -793,6 +804,7 @@ class BaseHandler(RequestHandler):
self.set_login_cookie(user)
self.statsd.incr('login.success')
self.statsd.timing('login.authenticate.success', auth_timer.ms)
self.log.info("User logged in: %s", user.name)
user._auth_refreshed = time.monotonic()
return user

View File

@@ -253,6 +253,19 @@ class User:
def spawner_class(self):
return self.settings.get('spawner_class', LocalProcessSpawner)
def sync_groups(self, user_groups):
"""Syncronize groups with database"""
if user_groups:
groups = (
self.db.query(orm.Group).filter(orm.Group.name.in_(user_groups)).all()
)
groups = {g.name: g for g in groups}
self.groups = [groups.get(g, orm.Group(name=g)) for g in user_groups]
else:
self.groups = []
async def save_auth_state(self, auth_state):
"""Encrypt and store auth_state"""
if auth_state is None: