user-initiated sharing (#4594)

Squashed merge of https://github.com/jupyterhub/jupyterhub/pull/4594

Co-authored-by: Samuel Gaist <samuel.gaist@idiap.ch>
This commit is contained in:
Min RK
2024-02-07 08:34:39 +01:00
committed by GitHub
parent 4555d5bbb2
commit 41fff711e7
36 changed files with 5756 additions and 345 deletions

View File

@@ -0,0 +1,77 @@
# User-initiated sharing
This example contains a jupyterhub configuration and sample notebooks demonstrating user-initiated sharing from within a JupyterLab session.
What _admins_ need to do is enable sharing:
```python
c.JupyterHub.load_roles = [
{
"name": "user",
"scopes": ["self", "shares!user"],
}
]
```
## Getting a token with sharing permission
Users can always issue themselves tokens with the desired permissions.
But for a deployment, it's likely that you want to grant sharing permission to something,
be it a service or some part of the single-user application.
There are two ways to do this in a single-user session,
and for convenience, this example includes both.
In most real deployments, it will only make sense to do one or the other.
### Sharing via JupyterLab extension
If you have a JupyterLab javascript sharing extension or server extension,
sharing permissions should be granted to the oauth tokens used to visit the single-user server.
These permissions can be specified:
```python
# OAuth token should have sharing permissions,
# so JupyterLab javascript can manage shares
c.Spawner.oauth_client_allowed_scopes = ["access:servers!server", "shares!server"]
```
The notebook `share-jupyterlab.ipynb` contains a few javascript snippets which will use the JupyterLab configuration to make API requests to JupyterHub from javascript in order to grant access.
This workflow _should_ be handled by a proper JupyterLab extension,
but this notebook of javascript snippets serves as a proof of concept for what is required to build such an extension.
## Sharing via API token
These same permissions can also be granted to the server token itself,
which is available as $JUPYTERHUB_API_TOKEN in the server,
as well as terminals and notebooks.
```python
# grant $JUPYTERHUB_API_TOKEN sharing permissions
# so that _python_ code can manage shares
c.Spawner.server_token_scopes = [
"shares!server", # manage shares
"servers!server", # start/stop itself
"users:activity!server", # report activity (default permission)
]
```
This method is not preferable, because it means anyone with _access_ to the server also has access to the token to grant further sharing permissions,
which is not the case when using the oauth permissions above,
where each visiting user has their own permissions.
But it is more convenient for demonstration purposes, because we can write a Python notebook to use it, share-api.ipynb.
## Run the example
First, launch jupyterhub: `jupyterhub`.
Then login as the user `sharer`.
Run the first couple of cells of the notebook, until you get a `/hub/accept-share` URL.
Open a new private browser window, and paste this URL. When prompted, login with the username `shared-with`.
In the end, you should arrive at `sharer`'s server as the user `shared-with`.
After visiting as `shared-with`, you can proceed in the notebook as `sharer` and view who has permissions, revoke share codes, permissions, etc.

View File

@@ -0,0 +1,37 @@
c = get_config() # noqa
c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple'
c.Authenticator.allowed_users = {"sharer", "shared-with"}
# put the current directory on sys.path for shareextension.py
from pathlib import Path
here = Path(__file__).parent.absolute()
c.Spawner.notebook_dir = str(here)
# users need sharing permissions for their own servers
c.JupyterHub.load_roles = [
{
"name": "user",
"scopes": ["self", "shares!user"],
},
]
# below are two ways to grant sharing permission to a single-user server.
# there's no reason to use both
# OAuth token should have sharing permissions,
# so JupyterLab javascript can manage shares
c.Spawner.oauth_client_allowed_scopes = ["access:servers!server", "shares!server"]
# grant $JUPYTERHUB_API_TOKEN sharing permissions
# so that _python_ code can manage shares
c.Spawner.server_token_scopes = [
"shares!server", # manage shares
"servers!server", # start/stop itself
"users:activity!server", # report activity
]

View File

@@ -0,0 +1,685 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8c5bd1ca-3329-4062-8851-9bd33009d805",
"metadata": {},
"source": [
"# Using the sharing API from Python \n",
"\n",
"In this example, we use $JUPYTERHUB_API_TOKEN to communicate with the sharing API via Python.\n",
"\n",
"The permissions used here are granted via the `c.Spawner.server_token_scopes` config in jupyterhub_config.py\n",
"\n",
"By using this token, any user who has access to this server has access to sharing permissions.\n",
"\n",
"First, get some useful configuration from the server environment:\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "95fa629b-ac65-46da-86a6-9798f3d0a2ba",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'http://127.0.0.1:8081/hub/api'"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import os\n",
"\n",
"hub_api = os.environ[\"JUPYTERHUB_API_URL\"]\n",
"token = os.environ[\"JUPYTERHUB_API_TOKEN\"]\n",
"username = os.environ[\"JUPYTERHUB_USER\"]\n",
"user_server = f\"{username}/{os.environ['JUPYTERHUB_SERVER_NAME']}\"\n",
"hub_host = os.environ[\"JUPYTERHUB_HOST\"]\n",
"server_base_url = os.environ[\"JUPYTERHUB_SERVICE_PREFIX\"]\n",
"\n",
"hub_api"
]
},
{
"cell_type": "markdown",
"id": "456697ab-e7ce-4f75-bc8f-3ce424830e0d",
"metadata": {},
"source": [
"Create a requests.Session to make jupyterhub API requests with our token"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f3335eeb-64e5-4caa-acb5-1032aaf727bb",
"metadata": {},
"outputs": [],
"source": [
"import requests\n",
"\n",
"session = requests.Session()\n",
"session.headers = {\"Authorization\": f\"Bearer {token}\"}"
]
},
{
"cell_type": "markdown",
"id": "3cf5605b-2022-4d2a-b0cc-de6f80e8f2fe",
"metadata": {},
"source": [
"We can check the permissions our token has with a request to /hub/api/user:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "17529af3-5ec8-4495-9183-bd5d423a1d76",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'kind': 'user',\n",
" 'last_activity': '2024-01-23T11:43:50.800864Z',\n",
" 'groups': [],\n",
" 'admin': False,\n",
" 'name': 'sharer',\n",
" 'servers': {'': {'name': '',\n",
" 'full_name': 'sharer/',\n",
" 'last_activity': '2024-01-23T11:43:50.800864Z',\n",
" 'started': '2024-01-23T11:28:44.948553Z',\n",
" 'pending': None,\n",
" 'ready': True,\n",
" 'stopped': False,\n",
" 'url': '/user/sharer/',\n",
" 'user_options': {},\n",
" 'progress_url': '/hub/api/users/sharer/server/progress'}},\n",
" 'session_id': None,\n",
" 'scopes': ['access:servers!server=sharer/',\n",
" 'delete:servers!server=sharer/',\n",
" 'groups:shares!server=sharer/',\n",
" 'read:groups:shares!server=sharer/',\n",
" 'read:servers!server=sharer/',\n",
" 'read:shares!server=sharer/',\n",
" 'read:users:activity!user=sharer',\n",
" 'read:users:groups!user=sharer',\n",
" 'read:users:name!user=sharer',\n",
" 'servers!server=sharer/',\n",
" 'shares!server=sharer/',\n",
" 'users:activity!server=sharer/',\n",
" 'users:activity!user=sharer',\n",
" 'users:shares!server=sharer/']}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"r = session.get(f\"{hub_api}/user\")\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"id": "a14578b8-577e-4b0a-b74c-40d7a04a1a1a",
"metadata": {},
"source": [
"We can see who has access to this server:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "63520e3c-3621-4ebf-9775-52e6150ccca5",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'items': [],\n",
" '_pagination': {'offset': 0, 'limit': 200, 'total': 0, 'next': None}}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"shares_url = f\"{hub_api}/shares/{user_server}\"\n",
"share_codes_url = f\"{hub_api}/share-codes/{user_server}\"\n",
"r = session.get(shares_url)\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"id": "5c8870d0-5b7e-4065-b227-fd463289cfdf",
"metadata": {},
"source": [
"and if there are any outstanding codes:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "6fad5334-6034-48f8-b008-1fd3d6e4d139",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'items': [],\n",
" '_pagination': {'offset': 0, 'limit': 200, 'total': 0, 'next': None}}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"r = session.get(share_codes_url)\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"id": "e6d598b3-5eab-4d56-b2ea-9aec345a3948",
"metadata": {},
"source": [
"Next, we can create a code:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "facb1872-44a5-4e4e-84d4-1fd829b1a27b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'server': {'user': {'name': 'sharer'},\n",
" 'name': '',\n",
" 'url': '/user/sharer/',\n",
" 'ready': True},\n",
" 'scopes': ['access:servers!server=sharer/'],\n",
" 'id': 'sc_1',\n",
" 'created_at': '2024-01-23T11:46:32.154416Z',\n",
" 'expires_at': '2024-01-24T11:46:32.153582Z',\n",
" 'exchange_count': 0,\n",
" 'last_exchanged_at': None,\n",
" 'code': 'gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg',\n",
" 'accept_url': '/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg'}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"r = session.post(share_codes_url)\n",
"code_info = r.json()\n",
"code_info"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "83e23f83-18cb-4e54-9683-68c2ab0fcfc2",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
" <a href='/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg'>share this link to grant access</a>\n",
" "
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from IPython.display import display, HTML\n",
"full_accept_url = f\"{hub_host}{code_info['accept_url']}\"\n",
"\n",
"display(\n",
" HTML(f\"\"\"\n",
" <a href='{full_accept_url}'>share this link to grant access</a>\n",
" \"\"\")\n",
")\n"
]
},
{
"cell_type": "markdown",
"id": "0ce4f6f8-a0f3-4556-ab25-1f86ef49a6db",
"metadata": {},
"source": [
"(in jupyterlab, shift-right-click to copy link)\n",
"\n",
"We can now give this to the shared-with user (i.e. us in another private browsing tab).\n",
"\n",
"After accepting the link, we can see who we've shared with again:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "e056a9f0-7cec-414b-a3ce-03c8abfbc087",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'items': [{'server': {'user': {'name': 'sharer'},\n",
" 'name': '',\n",
" 'url': '/user/sharer/',\n",
" 'ready': True},\n",
" 'scopes': ['access:servers!server=sharer/'],\n",
" 'user': {'name': 'shared-with'},\n",
" 'group': None,\n",
" 'kind': 'user',\n",
" 'created_at': '2024-01-23T11:46:43.585455Z'}],\n",
" '_pagination': {'offset': 0, 'limit': 200, 'total': 1, 'next': None}}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"session.get(shares_url).json()"
]
},
{
"cell_type": "markdown",
"id": "9933ef79-3f59-45fc-92a9-13e7bc1f3b82",
"metadata": {},
"source": [
"The share code can also include a `?next=` url parameter, to enable a link to take users to a specific file or view after accepting the code:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "9c313d99-4dc9-45af-9643-f2b0e08b2bb4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg&next=%2Fuser%2Fsharer%2Flab%2Ftree%2Fshare-api.ipynb\n"
]
},
{
"data": {
"text/html": [
"\n",
" share <a href='/hub/accept-share?code=gTs7DmIhFu6UG5VQkOzRMk2MN-cdDiQ_RONCnkaq5Cg&next=%2Fuser%2Fsharer%2Flab%2Ftree%2Fshare-api.ipynb'>this link</a>\n",
" to grant access and direct users to this notebook\n",
" "
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from urllib.parse import urlencode\n",
"\n",
"this_notebook_url = server_base_url + \"lab/tree/share-api.ipynb\"\n",
"this_notebook_accept_url = full_accept_url + \"&\" + urlencode({\"next\": this_notebook_url})\n",
"print(this_notebook_accept_url)\n",
"\n",
"display(\n",
" HTML(f\"\"\"\n",
" share <a href='{this_notebook_accept_url}'>this link</a>\n",
" to grant access and direct users to this notebook\n",
" \"\"\")\n",
")\n"
]
},
{
"cell_type": "markdown",
"id": "2455ba21-f558-4391-b8fc-6501feb3bbfb",
"metadata": {},
"source": [
"## Reviewing and managing access\n",
"\n",
"Listing share codes doesn't reveal the code - if you need to get a code, issue a new sharing code.\n",
"\n",
"But we can see in `exchange_count` whether and how often the code has been used"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "313116ed-2aa5-4a0a-bc91-f15a9428ae0c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'items': [{'server': {'user': {'name': 'sharer'},\n",
" 'name': '',\n",
" 'url': '/user/sharer/',\n",
" 'ready': True},\n",
" 'scopes': ['access:servers!server=sharer/'],\n",
" 'id': 'sc_1',\n",
" 'created_at': '2024-01-23T11:46:32.154416Z',\n",
" 'expires_at': '2024-01-24T11:46:32.153582Z',\n",
" 'exchange_count': 1,\n",
" 'last_exchanged_at': '2024-01-23T11:46:43.589701Z'}],\n",
" '_pagination': {'offset': 0, 'limit': 200, 'total': 1, 'next': None}}"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"session.get(share_codes_url).json()"
]
},
{
"cell_type": "markdown",
"id": "5f0d4d8e-fabb-4423-b9bd-d96643a9b9f7",
"metadata": {},
"source": [
"we can also revoke the code. Codes can be deleted by code or id"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "666ac28c-70f5-4a75-9bfa-6d26c5ea610f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Response [204]>"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"session.delete(share_codes_url + f\"?id={code_info['id']}\")"
]
},
{
"cell_type": "markdown",
"id": "ce2fdb7f-a2de-4c95-996b-527ce0536794",
"metadata": {},
"source": [
"or if you're done sharing via code, you can delete all sharing codes for a server without looking it up their ids:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "4633b0af-ad75-4e72-9702-9cb16182aecb",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Response [204]>"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"session.delete(share_codes_url)"
]
},
{
"cell_type": "markdown",
"id": "c322e058-bbf5-4058-8e20-415c7da0aa8a",
"metadata": {},
"source": [
"scopes and expiration can be customized in the request when creating the share code:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "462bcec7-a899-4e3e-8e77-7d08adedb7a4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Response [200]>"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import json\n",
"\n",
"options = {\n",
" \"scopes\": [\n",
" f\"access:servers!server={user_server}\", # access the server (default)\n",
" f\"servers!server={user_server}\", # start/stop the server\n",
" f\"shares!server={user_server}\", # further share the server with others\n",
" ],\n",
" \"expires_in\": 3600, # code expires in one hour\n",
"}\n",
"\n",
"\n",
"session.post(share_codes_url, data=json.dumps(options))\n"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "c8babe6c-6339-4990-9241-d3db54205108",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'items': [{'server': {'user': {'name': 'sharer'},\n",
" 'name': '',\n",
" 'url': '/user/sharer/',\n",
" 'ready': True},\n",
" 'scopes': ['access:servers!server=sharer/'],\n",
" 'user': {'name': 'shared-with'},\n",
" 'group': None,\n",
" 'kind': 'user',\n",
" 'created_at': '2024-01-23T11:46:43.585455Z'}],\n",
" '_pagination': {'offset': 0, 'limit': 200, 'total': 1, 'next': None}}"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"r = session.get(shares_url)\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"id": "616d8f46-940d-4527-98a7-27894a23974f",
"metadata": {},
"source": [
"## Revoking permissions\n",
"\n",
"We can revoke specific permssions via a PATCH request\n",
"by specifying the user (or group) and one or more scopes:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "13d54943-e95a-4222-a67d-8f3832b70e3c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Response [200]>"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"options = {\n",
" \"user\": \"shared-with\",\n",
" \"scopes\": ['shares!server=sharer/'],\n",
"}\n",
"\n",
"session.patch(shares_url, data=json.dumps(options))"
]
},
{
"cell_type": "markdown",
"id": "0b2b2619-3bb4-41a7-852b-de019f49185e",
"metadata": {},
"source": [
"If scopes are unspecified, all permissions are revoked:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "1d8997fa-7093-44dd-abdf-405fe8cc7fcd",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Response [200]>"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"options = {\n",
" \"user\": \"shared-with\",\n",
"}\n",
"\n",
"session.patch(shares_url, data=json.dumps(options))\n"
]
},
{
"cell_type": "markdown",
"id": "bb4e37a4-d206-420b-ad21-93a15c65bbd9",
"metadata": {},
"source": [
"_All_ shared access can be revoked via a DELETE request to the shares URL:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "203ec184-df77-493b-bd8f-e6bb461abe7e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<Response [204]>"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"session.delete(shares_url)"
]
},
{
"cell_type": "markdown",
"id": "129d98a3-df16-4607-8a9d-784843a7eeaf",
"metadata": {},
"source": [
"and we can see that nobody has shared access anymore"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "d40f97b8-171f-4958-9cbf-bcc08a5f0db7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'items': [],\n",
" '_pagination': {'offset': 0, 'limit': 200, 'total': 0, 'next': None}}"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"session.get(shares_url).json()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,403 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "7609a68a-c01b-43a8-90aa-3d004af614dd",
"metadata": {},
"source": [
"# Sharing access to a server\n",
"\n",
"This notebook executes some javascript in the browser, using the user's OAuth token.\n",
"\n",
"This code would normally reside in a jupyterlab extension.\n",
"The notebook serves only for demonstration purposes.\n",
"\n",
"First, collect some configuration from the page, so we can talk to the JupyterHub API:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "b3cf16bd-ff9b-4140-a394-5a7ca5d96d88",
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"// define some globals to share\n",
"\n",
"var configElement = document.getElementById(\"jupyter-config-data\");\n",
"var jupyterConfig = JSON.parse(configElement.innerHTML);\n",
"\n",
"window.token = jupyterConfig.token;\n",
"window.hubOrigin = `${document.location.protocol}//${jupyterConfig.hubHost || window.location.host}`\n",
"window.hubUrl = `${hubOrigin}${jupyterConfig.hubPrefix}`;\n",
"window.shareCodesUrl = `${hubUrl}api/share-codes/${jupyterConfig.hubServerUser}/${jupyterConfig.hubServerName}`\n",
"window.sharesUrl = `${hubUrl}api/shares/${jupyterConfig.hubServerUser}/${jupyterConfig.hubServerName}`\n",
"console.log(shareCodesUrl);\n",
"\n",
"// utility function to make API requests and parse errors\n",
"window.apiRequest = async function (url, options) {\n",
" var element = options.element;\n",
" var okStatus = options.ok || 200;\n",
" var resp = await fetch(url, {headers: {Authorization: `Bearer ${token}`}, method: options.method || 'GET'});\n",
" var replyText = await resp.text();\n",
" var replyJSON = {};\n",
" if (replyText.length) {\n",
" replyJSON = JSON.parse(replyText);\n",
" }\n",
" \n",
" if (resp.status != okStatus) {\n",
" var p = document.createElement('p');\n",
" p.innerText = `Error ${resp.status}: ${replyJSON.message}`;\n",
" element.appendChild(p);\n",
" return;\n",
" }\n",
" return replyJSON;\n",
"}\n",
"\n",
"// `element` is a special variable for the current cell's output area\n",
"element.innerText = `API URL for sharing codes is: ${shareCodesUrl}`;\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%%javascript\n",
"// define some globals to share\n",
"\n",
"var configElement = document.getElementById(\"jupyter-config-data\");\n",
"var jupyterConfig = JSON.parse(configElement.innerHTML);\n",
"\n",
"window.token = jupyterConfig.token;\n",
"window.hubOrigin = `${document.location.protocol}//${jupyterConfig.hubHost || window.location.host}`\n",
"window.hubUrl = `${hubOrigin}${jupyterConfig.hubPrefix}`;\n",
"window.shareCodesUrl = `${hubUrl}api/share-codes/${jupyterConfig.hubServerUser}/${jupyterConfig.hubServerName}`\n",
"window.sharesUrl = `${hubUrl}api/shares/${jupyterConfig.hubServerUser}/${jupyterConfig.hubServerName}`\n",
"console.log(shareCodesUrl);\n",
"\n",
"// utility function to make API requests and parse errors\n",
"window.apiRequest = async function (url, options) {\n",
" var element = options.element;\n",
" var okStatus = options.ok || 200;\n",
" var resp = await fetch(url, {headers: {Authorization: `Bearer ${token}`}, method: options.method || 'GET'});\n",
" var replyText = await resp.text();\n",
" var replyJSON = {};\n",
" if (replyText.length) {\n",
" replyJSON = JSON.parse(replyText);\n",
" }\n",
" \n",
" if (resp.status != okStatus) {\n",
" var p = document.createElement('p');\n",
" p.innerText = `Error ${resp.status}: ${replyJSON.message}`;\n",
" element.appendChild(p);\n",
" return;\n",
" }\n",
" return replyJSON;\n",
"}\n",
"\n",
"// `element` is a special variable for the current cell's output area\n",
"element.innerText = `API URL for sharing codes is: ${shareCodesUrl}`;"
]
},
{
"cell_type": "markdown",
"id": "b2049fa1-bb60-4073-9167-2e116b198f0e",
"metadata": {},
"source": [
"Next, we can request a share code with\n",
"\n",
"```\n",
"POST $hub/api/share-codes/$user/$server\n",
"```\n",
"\n",
"The URL for _accepting_ a sharing invitation code is `/hub/accept-share?code=abc123...`:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "ee9430f3-4866-41f7-942d-423bd48dc6b8",
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"\n",
"(async function f() {\n",
" var shareCode = await apiRequest(shareCodesUrl, {method: 'POST', element: element});\n",
"\n",
" // laziest way to display\n",
" var shareCodeUrl = `${hubOrigin}${shareCode.accept_url}`\n",
" var a = document.createElement('a');\n",
" a.href = shareCodeUrl;\n",
" a.innerText = shareCodeUrl;\n",
" var p = document.createElement(p);\n",
" p.append(\"Share this URL to grant access to this server: \");\n",
" p.appendChild(a);\n",
" element.appendChild(p);\n",
"})();\n",
"\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%%javascript\n",
"\n",
"(async function f() {\n",
" var shareCode = await apiRequest(shareCodesUrl, {method: 'POST', element: element});\n",
"\n",
" // laziest way to display\n",
" var shareCodeUrl = `${hubOrigin}${shareCode.accept_url}`\n",
" var a = document.createElement('a');\n",
" a.href = shareCodeUrl;\n",
" a.innerText = shareCodeUrl;\n",
" var p = document.createElement(p);\n",
" p.append(\"Share this URL to grant access to this server: \");\n",
" p.appendChild(a);\n",
" element.appendChild(p);\n",
"})();\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "418152a0-2e0f-4ca6-8b53-9295d15c4345",
"metadata": {},
"source": [
"Share this URL to grant access to your server (e.g. visit the URL in a private window and login as the user `shared-with`).\n",
"\n",
"After our code has been used, we can see who has access to this server:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "5c9c6e1b-ccab-4c85-8afb-db141651236a",
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"\n",
"(async function f() {\n",
"\n",
" var shares = await apiRequest(sharesUrl, {element: element});\n",
"\n",
" var list = document.createElement('ul');\n",
" for (var share of shares.items) {\n",
" var p = document.createElement('li');\n",
" p.append(`${share.kind} ${share[share.kind].name} has access: `)\n",
" var scopes = document.createElement('tt');\n",
" scopes.innerText = share.scopes.join(',');\n",
" p.appendChild(scopes);\n",
" list.append(p);\n",
" }\n",
" var p = document.createElement('p');\n",
" p.innerText = `Shared with ${shares.items.length} users:`;\n",
" element.appendChild(p);\n",
" element.appendChild(list);\n",
" return;\n",
"})();\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%%javascript\n",
"\n",
"(async function f() {\n",
"\n",
" var shares = await apiRequest(sharesUrl, {element: element});\n",
"\n",
" var list = document.createElement('ul');\n",
" for (var share of shares.items) {\n",
" var p = document.createElement('li');\n",
" p.append(`${share.kind} ${share[share.kind].name} has access: `)\n",
" var scopes = document.createElement('tt');\n",
" scopes.innerText = share.scopes.join(',');\n",
" p.appendChild(scopes);\n",
" list.append(p);\n",
" }\n",
" var p = document.createElement('p');\n",
" p.innerText = `Shared with ${shares.items.length} users:`;\n",
" element.appendChild(p);\n",
" element.appendChild(list);\n",
" return;\n",
"})();\n"
]
},
{
"cell_type": "markdown",
"id": "18165205-67f3-44d4-ad6b-40ab27ec469c",
"metadata": {},
"source": [
"We could also use this info to revoke permissions, or share with individuals by name.\n",
"\n",
"We can also review outstanding sharing _codes_:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "40cd1e2d-fc21-4238-8cbc-9ae45be248c9",
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"\n",
"(async function f() {\n",
" var shareCodes = await apiRequest(shareCodesUrl, {element: element});\n",
" var p = document.createElement('pre');\n",
" p.innerText = JSON.stringify(shareCodes.items, null, ' ');\n",
" element.appendChild(p);\n",
"})();\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%%javascript\n",
"\n",
"(async function f() {\n",
" var shareCodes = await apiRequest(shareCodesUrl, {element: element});\n",
" var p = document.createElement('pre');\n",
" p.innerText = JSON.stringify(shareCodes.items, null, ' ');\n",
" element.appendChild(p);\n",
"})();\n"
]
},
{
"cell_type": "markdown",
"id": "0effee22-c132-4b44-a677-c4375594c462",
"metadata": {},
"source": [
"And finally, when we're done, we can revoke the codes, at which point nobody _new_ can use the code to gain access to this server,\n",
"but anyone who has accepted the code will still have access:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "51a795f5-9a74-49b1-9ef3-e7978da0cc44",
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"\n",
"(async function f() {\n",
" await apiRequest(shareCodesUrl, {method: 'DELETE', element: element, ok: 204});\n",
" var p = document.createElement('p');\n",
" p.innerText = `Deleted all share codes`;\n",
" element.appendChild(p); \n",
"})();\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%%javascript\n",
"\n",
"(async function f() {\n",
" await apiRequest(shareCodesUrl, {method: 'DELETE', element: element, ok: 204});\n",
" var p = document.createElement('p');\n",
" p.innerText = `Deleted all share codes`;\n",
" element.appendChild(p); \n",
"})();"
]
},
{
"cell_type": "markdown",
"id": "2ad12718-1f68-4d2f-9934-dd6bd4555a1d",
"metadata": {},
"source": [
"Or even revoke all shared access, so anyone who may have used the code no longer has any access:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "efa5ae15-ac99-4bf4-b7b4-3d93747dba8c",
"metadata": {},
"outputs": [
{
"data": {
"application/javascript": [
"\n",
"(async function f() {\n",
" var resp = await apiRequest(sharesUrl, {method: 'DELETE', element: element, ok: 204});\n",
" var p = document.createElement('p');\n",
" p.innerText = `Deleted all shared access`;\n",
" element.appendChild(p); \n",
"})();\n"
],
"text/plain": [
"<IPython.core.display.Javascript object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"%%javascript\n",
"\n",
"(async function f() {\n",
" var resp = await apiRequest(sharesUrl, {method: 'DELETE', element: element, ok: 204});\n",
" var p = document.createElement('p');\n",
" p.innerText = `Deleted all shared access`;\n",
" element.appendChild(p); \n",
"})();"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}