Skip to content

Commit

Permalink
Merge pull request #1239 from yuvipanda/oauthenticator
Browse files Browse the repository at this point in the history
Restrict access to profiles based on GH team membership
  • Loading branch information
yuvipanda authored Apr 26, 2022
2 parents d6a5bad + 9f40eb7 commit 7250a63
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 1 deletion.
15 changes: 15 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,40 @@ 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-education
- 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
71 changes: 71 additions & 0 deletions docs/howto/configure/auth-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,77 @@ If administrators report a `403 forbidden` error when they try to log in to the
In this case, they should go to the configuration page for this app within their GitHub organization and explicitly grant it access.
See [the GitHub apps for organizations docs](https://docs.github.com/en/organizations/managing-access-to-your-organizations-apps) for more information.

### Restricting user profiles based on GitHub Team Membership

JupyterHub has support for using [profileList](https://zero-to-jupyterhub.readthedocs.io/en/latest/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment)
to give users a choice of machine sizes and images to choose from when launching their
server.

In addition, we can allow people access to specific profiles based on their GitHub Teams membership!
This only works if the hub is already set to allow people only from certain GitHub organizations
to log in.

The key `allowed_teams` can be set for any profile definition, with a list of GitHub
teams (formatted as `<github-org>:<team-name>`) that will get access to that profile. Users
need to be a member of any one of the listed teams for access. The list of teams a user
is part of is fetched at login time - so if the user is added to a GitHub team, they need
to log out and log back in to the JupyterHub (not necessarily to GitHub!) to see the new
profiles they have access to. To remove access to a profile from a user, they have to be
removed from the appropriate team on GitHub *and* their JupyterHub user needs to be
deleted from the hub admin dashboard.

To enable this access,

1. Enable storing the list of GitHub teams a user is in as a part of
[`auth_state`](https://zero-to-jupyterhub.readthedocs.io/en/latest/administrator/authentication.html#enable-auth-state)
with the following config:

```yaml
jupyterhub:
hub:
config:
Authenticator:
enable_auth_state: true
GitHubOAuthenticator:
populate_teams_in_auth_state: true
```

If `populate_teams_in_auth_state` is not set, this entire feature is disabled.

2. Specify which teams should have access to which profiles with an `allowed_teams` key
under `profileList`:

```yaml
jupyterhub:
singleuser:
profileList:
- display_name: "Small"
description: 5GB RAM, 2 CPUs
default: true
allowed_teams:
- <org-name>:<team-name>
- 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:
- <org-name>:<team-name>
- 2i2c-org:tech-team
kubespawner_override:
mem_limit: 15G
mem_guarantee: 11G
node_selector:
node.kubernetes.io/instance-type: n1-standard-4
```

Users who are a part of *any* of the listed teams will be able to access that profile.
Add `2i2c-org:teach-team` to all `allowed_teams` so 2i2c engineers can log in to debug
issues. If `allowed_teams` is not set, that profile is not available to anyone.

## CILogon

[CILogon](https://www.cilogon.org) is a service provider that allows users to login against various identity providers, including campus identity providers. 2i2c can manage CILogon either using the JupyterHub CILogonOAuthenticator or through [auth0](https://auth0.com), similar to Google and GitHub authentication.
Expand Down
73 changes: 72 additions & 1 deletion helm-charts/basehub/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ jupyterhub:
admin: true
image:
name: quay.io/2i2c/pilot-hub
tag: "0.0.1-n2546.ha1b6098"
tag: "0.0.1-n3027.h52c5e2a"
nodeSelector:
hub.jupyter.org/node-purpose: core
networkPolicy:
Expand Down Expand Up @@ -441,3 +441,74 @@ 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.
"""
# If populate_teams_in_auth_state is not set, github teams are not fetched
# So we just don't do any of this filtering, and let anyone into everything
if spawner.authenticator.populate_teams_in_auth_state == False:
return original_profile_list
auth_state = yield spawner.user.get_auth_state()
if not auth_state or "teams" not in auth_state:
if spawner.user.name == 'deployment-service-check':
# For our hub deployer health checker, ignore all this logic
print("Ignoring allowed_teams check for deployment-service-check")
return original_profile_list
print(f"User {spawner.user.name} does not have any auth_state set")
raise web.HTTPError(403)
# 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
3 changes: 3 additions & 0 deletions helm-charts/images/hub/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ RUN pip install --no-cache git+https://github.com/yuvipanda/jupyterhub-configura
# z2jh 1.2.x ships with kubespawner 1.1.0, so we just do a little bump
RUN pip install --no-cache --upgrade jupyterhub-kubespawner==1.1.2

# Brings in https://github.com/jupyterhub/oauthenticator/pull/498
RUN pip install --no-cache --upgrade git+https://github.com/yuvipanda/oauthenticator@7f3fdc0a14d06f1a081c23b1ceb7060a940d11f8

USER root
RUN mkdir -p /usr/local/etc/jupyterhub-configurator

Expand Down

0 comments on commit 7250a63

Please sign in to comment.