Skip to content

Commit

Permalink
Restrict access to profiles based on GH team membership
Browse files Browse the repository at this point in the history
profile_list is now dynamically generated, based on the GH teams
user is a part of. This list of teams is refreshed only during login -
so user needs to log out and log back in to see new teams! This also
means that users removed from teams on GH will still have access to
the profiles until they are logged out from the admin panel too (to
be fixed)

This approach is taken over customizing options_form to protect
against users just bypassing the options form and using the API
directly to spawn servers.

Deployed to the leap hub, except 'large' & 'huge' is only available to
leap-stc:leap-pangeo-research members, not to leap-stc:leap-pangeo-users
members - based on 2i2c-org#1050 (comment)

Fixes 2i2c-org#1146
  • Loading branch information
yuvipanda committed Apr 25, 2022
1 parent de3264b commit 97eff99
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 0 deletions.
14 changes: 14 additions & 0 deletions config/clusters/leap/common.values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ basehub:
allowNamedServers: true
config:
Authenticator:
enable_auth_state: true
# This hub uses GitHub Teams auth and so we don't set
# allowed_users in order to not deny access to valid members of
# the listed teams. These people should have admin access though.
Expand All @@ -44,6 +45,7 @@ basehub:
JupyterHub:
authenticator_class: github
GitHubOAuthenticator:
populate_teams_in_auth_state: true
allowed_organizations:
- leap-stc:leap-pangeo-users
- 2i2c-org:tech-team
Expand All @@ -67,27 +69,39 @@ basehub:
- display_name: "Small"
description: 5GB RAM, 2 CPUs
default: true
allowed_teams:
- leap-stc:leap-pangeo-users
- 2i2c-org:tech-team
kubespawner_override:
mem_limit: 7G
mem_guarantee: 4.5G
node_selector:
node.kubernetes.io/instance-type: n1-standard-2
- display_name: Medium
description: 11GB RAM, 4 CPUs
allowed_teams:
- leap-stc:leap-pangeo-users
- 2i2c-org:tech-team
kubespawner_override:
mem_limit: 15G
mem_guarantee: 11G
node_selector:
node.kubernetes.io/instance-type: n1-standard-4
- display_name: Large
description: 24GB RAM, 8 CPUs
allowed_teams:
- leap-stc:leap-pangeo-research
- 2i2c-org:tech-team
kubespawner_override:
mem_limit: 30G
mem_guarantee: 24G
node_selector:
node.kubernetes.io/instance-type: n1-standard-8
- display_name: Huge
description: 52GB RAM, 16 CPUs
allowed_teams:
- leap-stc:leap-pangeo-research
- 2i2c-org:tech-team
kubespawner_override:
mem_limit: 60G
mem_guarantee: 52G
Expand Down
57 changes: 57 additions & 0 deletions helm-charts/basehub/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,60 @@ jupyterhub:
if get_config("custom.docs_service.enabled"):
c.JupyterHub.services.append({"name": "docs", "url": "http://docs-service"})
09-gh-teams: |
from textwrap import dedent
from tornado import gen, web
# Make a copy of the original profile_list, as that is the data we will work with
original_profile_list = c.KubeSpawner.profile_list
# This has to be a gen.coroutine, not async def! Kubespawner uses gen.maybe_future to
# run this, and that only seems to recognize tornado coroutines, not async functions!
# We can convert this to async def once that has been fixed upstream.
@gen.coroutine
def custom_profile_list(spawner):
"""
Dynamically set allowed list of user profiles based on GitHub teams user is part of.
Adds a 'allowed_teams' key to profile_list, with a list of GitHub teams (of the form
org-name:team-name) for which the profile is made available.
If the user isn't part of any team whose membership grants them access to even a single
profile, they aren't allowed to start any servers.
"""
auth_state = yield spawner.user.get_auth_state()
# Make a list of team names of form org-name:team-name
# This is the same syntax used by allowed_organizations traitlet of GitHubOAuthenticator
teams = set([f'{team_info["organization"]["login"]}:{team_info["slug"]}' for team_info in auth_state["teams"]])
allowed_profiles = []
for profile in original_profile_list:
# Keep the profile is the user is part of *any* team listed in allowed_teams
# If allowed_teams is empty or not set, it'll not be accessible to *anyone*
if set(profile.get('allowed_teams', [])) & teams:
allowed_profiles.append(profile)
print(f"Allowing profile {profile['display_name']} for user {spawner.user.name}")
else:
print(f"Dropping profile {profile['display_name']} for user {spawner.user.name}")
if len(allowed_profiles) == 0:
# If no profiles are allowed, user should not be able to spawn anything!
# If we don't explicitly stop this, user will be logged into the 'default' settings
# set in singleuser, without any profile overrides. Not desired behavior
# FIXME: User doesn't actually see this error message, just the generic 403.
error_msg = dedent(f"""
Your GitHub team membership is insufficient to launch any server profiles.
GitHub teams you are a member of that this JupyterHub knows about are {', '.join(teams)}.
If you are part of additional teams, log out of this JupyterHub and log back in to refresh that information.
""")
raise web.HTTPError(403, error_msg)
return allowed_profiles
# Customize list of profiles dynamically, rather than override options form.
# This is more secure, as users can't override the options available to them via the hub API
c.KubeSpawner.profile_list = custom_profile_list

0 comments on commit 97eff99

Please sign in to comment.