{ "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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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": [ "" ] }, "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 }