Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
jupyterhub-singleuser as a Jupyter Server 2.0 extension
mostly a copy (fork) of singleuser app using public APIs instead of lots of patching. opt-in via `JUPYTERHUB_SINGLEUSER_EXTENSION=1` related changes: - stop running a test single-user server in a thread. It's complicated and fragile. Instead, run it normally, and get the info we need from a custom handler registered via an extension via the `full_spawn` fixture
- Loading branch information
Showing
16 changed files
with
1,068 additions
and
142 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Granting read-only access to user servers | ||
|
||
Jupyter Server 2.0 adds the ability to enforce granular permissions via its Authorizer API. | ||
Combining this with JupyterHub's custom scopes and singleuser-server extension, you can use JupyterHub to grant users restricted (e.g. read-only) access to each others' servers, | ||
rather than an all-or-nothing access permission. | ||
|
||
This example demonstrates granting read-only access to just one specific user's server to one specific other user, | ||
but you can grant access for groups, all users, etc. | ||
Given users `vex` and `percy`, we want `vex` to have permission to: | ||
|
||
1. read and open files, and view the state of the server, but | ||
2. not write or edit files | ||
3. not start/stop the server | ||
4. not execute anything | ||
|
||
(Jupyter Server's Authorizer API allows for even more fine-grained control) | ||
|
||
To test this, you'll want two browser sessions: | ||
|
||
1. login as `percy` (dummy auth means username and no password needed) and start the server | ||
2. in another session (another browser, or logout as percy), login as `vex` (again, any password in the example) | ||
3. as vex, visit http://127.0.0.1:8000/users/percy/ | ||
|
||
Percy can use their server as normal, but vex will only be able to read files. | ||
Vex won't be able to run any code, connect to kernels, or save edits to files. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import os | ||
|
||
from jupyterhub.singleuser.extension import JupyterHubAuthorizer | ||
|
||
|
||
class GranularJupyterHubAuthorizer(JupyterHubAuthorizer): | ||
"""Authorizer that looks for permissions in JupyterHub scopes""" | ||
|
||
def is_authorized(self, handler, user, action, resource): | ||
# authorize if any of these permissions are present | ||
# filters check for access to this specific user or server | ||
# group filters aren't available! | ||
filters = [ | ||
f"!user={os.environ['JUPYTERHUB_USER']}", | ||
f"!server={os.environ['JUPYTERHUB_USER']}/{os.environ['JUPYTERHUB_SERVER_NAME']}", | ||
] | ||
required_scopes = set() | ||
for f in filters: | ||
required_scopes.update( | ||
{ | ||
f"custom:jupyter_server:{action}:{resource}{f}", | ||
f"custom:jupyter_server:{action}:*{f}", | ||
} | ||
) | ||
|
||
have_scopes = self.hub_auth.check_scopes(required_scopes, user.hub_user) | ||
self.log.debug( | ||
f"{user.username} has permissions {have_scopes} required to {action} on {resource}" | ||
) | ||
return bool(have_scopes) | ||
|
||
|
||
c = get_config() # noqa | ||
|
||
|
||
c.ServerApp.authorizer_class = GranularJupyterHubAuthorizer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
c = get_config() # noqa | ||
|
||
# define custom scopes so they can be assigned to users | ||
# these could be | ||
|
||
c.JupyterHub.custom_scopes = { | ||
"custom:jupyter_server:read:*": { | ||
"description": "read-only access to your server", | ||
}, | ||
"custom:jupyter_server:write:*": { | ||
"description": "access to modify files on your server. Does not include execution.", | ||
"subscopes": ["custom:jupyter_server:read:*"], | ||
}, | ||
"custom:jupyter_server:execute:*": { | ||
"description": "Execute permissions on servers.", | ||
"subscopes": [ | ||
"custom:jupyter_server:write:*", | ||
"custom:jupyter_server:read:*", | ||
], | ||
}, | ||
} | ||
|
||
c.JupyterHub.load_roles = [ | ||
# grant specific users read-only access to all servers | ||
{ | ||
"name": "read-only-all", | ||
"scopes": [ | ||
"access:servers", | ||
"custom:jupyter_server:read:*", | ||
], | ||
"groups": ["read-only"], | ||
}, | ||
{ | ||
"name": "read-only-read-only-percy", | ||
"scopes": [ | ||
"access:servers!user=percy", | ||
"custom:jupyter_server:read:*!user=percy", | ||
], | ||
"users": ["vex"], | ||
}, | ||
{ | ||
"name": "admin-ui", | ||
"scopes": [ | ||
"admin-ui", | ||
"list:users", | ||
"admin:servers", | ||
], | ||
"users": ["admin"], | ||
}, | ||
{ | ||
"name": "full-access", | ||
"scopes": [ | ||
"access:servers", | ||
"custom:jupyter_server:execute:*", | ||
], | ||
"users": ["minrk"], | ||
}, | ||
# all users have full access to their own servers | ||
{ | ||
"name": "user", | ||
"scopes": [ | ||
"custom:jupyter_server:execute:*!user", | ||
"custom:jupyter_server:read:*!user", | ||
"self", | ||
], | ||
}, | ||
] | ||
|
||
# servers request access to themselves | ||
|
||
c.Spawner.oauth_client_allowed_scopes = [ | ||
"access:servers!server", | ||
"custom:jupyter_server:read:*!server", | ||
"custom:jupyter_server:execute:*!server", | ||
] | ||
|
||
# enable the jupyter-server extension | ||
c.Spawner.environment = { | ||
"JUPYTERHUB_SINGLEUSER_EXTENSION": "1", | ||
} | ||
|
||
from pathlib import Path | ||
|
||
here = Path(__file__).parent.resolve() | ||
|
||
# load the server config that enables granular permissions | ||
c.Spawner.args = [ | ||
f"--config={here}/jupyter_server_config.py", | ||
] | ||
|
||
|
||
# example boilerplate: dummy auth/spawner | ||
c.JupyterHub.authenticator_class = 'dummy' | ||
c.JupyterHub.spawner_class = 'simple' | ||
c.JupyterHub.ip = '127.0.0.1' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,10 @@ | ||
from ._version import __version__, version_info | ||
|
||
|
||
def _jupyter_server_extension_points(): | ||
from .singleuser.extension import JupyterHubSingleUser | ||
|
||
return [{"module": "jupyterhub", "app": JupyterHubSingleUser}] | ||
|
||
|
||
__all__ = ["__version__", "version_info"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
from .app import main | ||
from . import main | ||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.