mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 06:52:59 +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')
|
@needs_scope('admin:groups')
|
||||||
async def post(self):
|
async def post(self):
|
||||||
"""POST creates Multiple groups"""
|
"""POST creates Multiple groups"""
|
||||||
|
|
||||||
|
if self.authenticator.manage_groups:
|
||||||
|
raise web.HTTPError(400, "Group management via API is disabled")
|
||||||
|
|
||||||
model = self.get_json_body()
|
model = self.get_json_body()
|
||||||
if not model or not isinstance(model, dict) or not model.get('groups'):
|
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")
|
raise web.HTTPError(400, "Must specify at least one group to create")
|
||||||
|
@@ -2001,6 +2001,9 @@ class JupyterHub(Application):
|
|||||||
async def init_groups(self):
|
async def init_groups(self):
|
||||||
"""Load predefined groups into the database"""
|
"""Load predefined groups into the database"""
|
||||||
db = self.db
|
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():
|
for name, usernames in self.load_groups.items():
|
||||||
group = orm.Group.find(db, name)
|
group = orm.Group.find(db, name)
|
||||||
if group is None:
|
if group is None:
|
||||||
|
@@ -635,6 +635,30 @@ class Authenticator(LoggingConfigurable):
|
|||||||
"""
|
"""
|
||||||
self.allowed_users.discard(user.name)
|
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(
|
auto_login = Bool(
|
||||||
False,
|
False,
|
||||||
config=True,
|
config=True,
|
||||||
|
@@ -622,6 +622,9 @@ class BaseHandler(RequestHandler):
|
|||||||
def authenticate(self, data):
|
def authenticate(self, data):
|
||||||
return maybe_future(self.authenticator.get_authenticated_user(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):
|
def get_next_url(self, user=None, default=None):
|
||||||
"""Get the next_url for login redirect
|
"""Get the next_url for login redirect
|
||||||
|
|
||||||
@@ -779,7 +782,15 @@ class BaseHandler(RequestHandler):
|
|||||||
if not self.authenticator.enable_auth_state:
|
if not self.authenticator.enable_auth_state:
|
||||||
# auth_state is not enabled. Force None.
|
# auth_state is not enabled. Force None.
|
||||||
auth_state = 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)
|
await user.save_auth_state(auth_state)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def login_user(self, data=None):
|
async def login_user(self, data=None):
|
||||||
@@ -793,6 +804,7 @@ class BaseHandler(RequestHandler):
|
|||||||
self.set_login_cookie(user)
|
self.set_login_cookie(user)
|
||||||
self.statsd.incr('login.success')
|
self.statsd.incr('login.success')
|
||||||
self.statsd.timing('login.authenticate.success', auth_timer.ms)
|
self.statsd.timing('login.authenticate.success', auth_timer.ms)
|
||||||
|
|
||||||
self.log.info("User logged in: %s", user.name)
|
self.log.info("User logged in: %s", user.name)
|
||||||
user._auth_refreshed = time.monotonic()
|
user._auth_refreshed = time.monotonic()
|
||||||
return user
|
return user
|
||||||
|
@@ -253,6 +253,19 @@ class User:
|
|||||||
def spawner_class(self):
|
def spawner_class(self):
|
||||||
return self.settings.get('spawner_class', LocalProcessSpawner)
|
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):
|
async def save_auth_state(self, auth_state):
|
||||||
"""Encrypt and store auth_state"""
|
"""Encrypt and store auth_state"""
|
||||||
if auth_state is None:
|
if auth_state is None:
|
||||||
|
Reference in New Issue
Block a user