Files
jupyterhub/examples/user-sharing/share-api.ipynb
2024-03-11 09:39:10 +01:00

689 lines
17 KiB
Plaintext

{
"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 HTML, display\n",
"\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",
")"
]
},
{
"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 = (\n",
" full_accept_url + \"&\" + urlencode({\"next\": this_notebook_url})\n",
")\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",
")"
]
},
{
"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))"
]
},
{
"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))"
]
},
{
"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
}