{ "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", " share this link to grant access\n", " " ], "text/plain": [ "" ] }, "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", " share this link to grant access\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 this link\n", " to grant access and direct users to this notebook\n", " " ], "text/plain": [ "" ] }, "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 this link\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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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 }