mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
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:

committed by
Min RK

parent
dcf21d53fd
commit
144abcb965
30
examples/azuread-with-group-management/jupyterhub_config.py
Normal file
30
examples/azuread-with-group-management/jupyterhub_config.py
Normal 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
|
2
examples/azuread-with-group-management/requirements.txt
Normal file
2
examples/azuread-with-group-management/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
oauthenticator
|
||||
pyjwt
|
@@ -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")
|
||||
|
@@ -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:
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user