{ "cells": [ { "cell_type": "markdown", "id": "192d2500-9007-4775-ae00-1f3216637258", "metadata": {}, "source": [ "\"Oasis\n", "


\n", "\n", "Exercise 4: User Access Management \n", "==================================\n", "This exercise is a short introduction on limiting access to models, portfolios and analyses.\n", "There are handled via group permissions in [KeyCloak](https://www.keycloak.org/documentation), an open-source identity and access management tool." ] }, { "cell_type": "code", "execution_count": null, "id": "5a015cbe-771e-4641-90f7-026c0f997b0c", "metadata": {}, "outputs": [], "source": [ "# Edit this value to match your workshop ID, if your username is `workshop6@oasislmfenterprise.onmicrosoft.com` set 'WORKSHOP_ID=6'\n", "WORKSHOP_ID=\n", "\n", "\n", "# Install of workshop package \n", "!pip install oasis-workshop==1.0.0\n", " \n", "# Clear cell output\n", "from IPython.display import clear_output\n", "clear_output(wait=False) " ] }, { "cell_type": "code", "execution_count": null, "id": "0a40c06d-87f4-44e5-90cb-9a67696b914b", "metadata": {}, "outputs": [], "source": [ "from oasis_workshop.client import APIClient\n", "from oasis_workshop.funcs import (\n", " tabulate_endpoint, \n", " tabulate_json,\n", " tabulate_analysis,\n", " tabulate_portfolio\n", ")\n", " \n", "from IPython.display import (\n", " display, \n", " Code,\n", " clear_output, \n", " HTML, \n", " JSON,\n", " Markdown \n", ")\n", "\n", "display(Markdown(f'# Keycloak URL - https://oasis-workshop-{WORKSHOP_ID}.northcentralus.cloudapp.azure.com/auth/'))" ] }, { "cell_type": "markdown", "id": "8d3bacb4-9e8c-4492-b702-89ba9b305cdf", "metadata": {}, "source": [ "## 4.1 KeyCloak setup " ] }, { "cell_type": "markdown", "id": "69f7f3dc-e83b-4e8f-9d10-5f3a6025009b", "metadata": {}, "source": [ "### 4.1.1 Import groups and users to KeyCloak \n", "Open the URL above and login using\n", "\n", "```\n", "username: keycloak\n", "password: password\n", "```\n" ] }, { "cell_type": "markdown", "id": "355b2992-1c8c-4437-b132-a3899162a563", "metadata": {}, "source": [ "#### 1. Save a local copy of the user and groups data \n", "Download the file [\n", "users-of-middle-earth.json](https://raw.githubusercontent.com/OasisLMF/Workshop2022/main/examples/users-of-middle-earth.json)\n", "\n", "\n", "#### 2. Go to the **import** page in Keycloak\n", "From there, select the saved `users-of-middle-earth.json` file from your PC.\n", "\n", "\"chunking\"\n", "\n", "#### 3. Import and Override \n", "Once loaded the page should look like the screenshot below, select `overwrite` from the dropdown and hit import.\n", "\n", "\"chunking\"" ] }, { "cell_type": "markdown", "id": "c164f4f2-dd10-4ec6-9864-d0faab2ee1fd", "metadata": {}, "source": [ "### 4.1.2 Start user sessions " ] }, { "cell_type": "code", "execution_count": null, "id": "fb648735-2ff1-4930-91f8-122d97e4fb6a", "metadata": {}, "outputs": [], "source": [ "server_url = f'https://oasis-workshop-{WORKSHOP_ID}.northcentralus.cloudapp.azure.com/api/'\n", "display_cols = ['id', 'supplier_id', 'model_id', 'version_id', 'groups']\n", "\n", "# Start API connections\n", "session_iluvatar = APIClient(api_url=server_url, username='iluvatar', password='pass')\n", "session_faramir = APIClient(api_url=server_url, username='faramir', password='pass')\n", "session_frodo = APIClient(api_url=server_url, username='frodo', password='pass')\n", "session_gandalf = APIClient(api_url=server_url, username='gandalf', password='notpass')\n", "session_gollum = APIClient(api_url=server_url, username='gollum', password='pass')\n", "session_sauron = APIClient(api_url=server_url, username='sauron', password='pass')" ] }, { "cell_type": "markdown", "id": "305caeec-b3c0-4d90-a937-64c6dc2df75d", "metadata": {}, "source": [ "## 4.2 Admin accounts can see all Objects.\n", "From the connected sessions only the account `iluvatar` is admin, the others are locked into set groups which were imported along with the user data. \n", "\n", "\n", "| **Username** | **Groups** | **is_admin** |\n", "|:-------------|------------|--------------|\n", "| iluvatar | admin, middle_earth | True |\n", "| faramir | gondor | False |\n", "| frodo | the_shire | False |\n", "| gandalf | gondor, the_shire | False |\n", "| gollum | -None- | False |\n", "| sauron | mordor | False |\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "1bdd8690-3f92-4be6-9e21-226b8564a070", "metadata": {}, "outputs": [], "source": [ "display(Markdown(f\"### Iluvatar's CAT Models\"))\n", "display(Markdown(f\"Note that the `groups` column is empty, thats because these were created by an admin account from ex2\"))\n", "display(HTML(tabulate_endpoint(session_iluvatar.models, display_cols)))\n", "\n", "display(Markdown(f\"### All the non-admin users return empty lists\"))\n", "display(Markdown(f\"The same applies when logging into the OasisUI with these accounts\"))\n", "display(Markdown(f\" * faramir - {session_faramir.models.get().json()}\"))\n", "display(Markdown(f\" * frodo - {session_frodo.models.get().json()}\"))\n", "display(Markdown(f\" * gandalf - {session_gandalf.models.get().json()}\"))\n", "display(Markdown(f\" * sauron - {session_sauron.models.get().json()}\"))" ] }, { "cell_type": "markdown", "id": "cf32ff65-5899-4ec9-8eae-f5fd3101fc72", "metadata": {}, "source": [ "## 4.3 User created Objects inherit group permissions\n", "This applies to `models` and `portfolios`" ] }, { "cell_type": "code", "execution_count": null, "id": "4b49f01a-5bdd-46b6-ad6f-a7025b395909", "metadata": {}, "outputs": [], "source": [ "if not session_faramir.models.search({'supplier_id': 'minas_tirith'}).json():\n", " session_faramir.models.create('minas_tirith', 'orc_interruption', '6.0')\n", "if not session_frodo.models.search({'supplier_id': 'mordor'}).json():\n", " session_frodo.models.create('mordor', 'travel_risk', '0.01')\n", "if not session_frodo.models.search({'supplier_id': 'hobbiton'}).json():\n", " session_frodo.models.create('hobbiton', 'horticulture', '3')\n", "if not session_gandalf.models.search({'supplier_id': 'rohan'}).json():\n", " session_gandalf.models.create('rohan', 'support_response_time', '2')\n", "if not session_sauron.models.search({'supplier_id': 'mount_doom'}).json():\n", " session_sauron.models.create('mount_doom', 'eruption', '1.0')\n", "if not session_sauron.models.search({'supplier_id': 'one_ring'}).json():\n", " session_sauron.models.create('one_ring', 'detection', '1.0')\n", " \n", "# Patch orig groups\n", "rohan_model = session_gandalf.models.search({'supplier_id': 'rohan'}).json().pop()\n", "session_gandalf.models.patch(rohan_model['id'], {'groups': ['gondor', 'the_shire']})" ] }, { "cell_type": "code", "execution_count": null, "id": "39c67b48-7f3f-41ad-a964-9c7e40ee0926", "metadata": {}, "outputs": [], "source": [ "display(Markdown(f\"### Faramir's Models\"))\n", "display(Markdown(f\"Note that the `groups` column is empty, thats because these were created by an admin account from ex2\"))\n", "display(HTML(tabulate_endpoint(session_faramir.models, display_cols)))\n", "\n", "display(Markdown(f\"### Frodo's CAT Models\"))\n", "display(Markdown(f\"Note that the `groups` column is empty, thats because these were created by an admin account from ex2\"))\n", "display(HTML(tabulate_endpoint(session_frodo.models, display_cols)))\n", "\n", "display(Markdown(f\"### Gandalf's CAT Models\"))\n", "display(Markdown(f\"Note that the `groups` column is empty, thats because these were created by an admin account from ex2\"))\n", "display(HTML(tabulate_endpoint(session_gandalf.models, display_cols)))\n", "\n", "display(Markdown(f\"### Sauron's CAT Models\"))\n", "display(Markdown(f\"Note that the `groups` column is empty, thats because these were created by an admin account from ex2\"))\n", "display(HTML(tabulate_endpoint(session_sauron.models, display_cols)))\n", "\n", "display(Markdown(f\"### Iluvatar's CAT Models\"))\n", "display(Markdown(f\"Note that the `groups` column is empty, thats because these were created by an admin account from ex2\"))\n", "display(HTML(tabulate_endpoint(session_iluvatar.models, display_cols)))" ] }, { "cell_type": "markdown", "id": "360192a8-3fba-496a-9546-1a3321ca0793", "metadata": {}, "source": [ "### 4.3.1 Empty groups are treated as a group\n", "\n", "A blank group assigment `[]` is treated as an [empty set](https://en.wikipedia.org/wiki/Empty_set), so like its own group. If a user belongs to a group it won't be able to access objects that belong to another group and vice versa. Since `gollum` is a member of the 'empty' group he can view the two models from ex2 -- which also have no groups assgigned." ] }, { "cell_type": "code", "execution_count": null, "id": "edb10f8f-68a3-4ded-a97b-16f02e167f1f", "metadata": {}, "outputs": [], "source": [ "display(Markdown(f\"### Gollum's CAT Models\"))\n", "display(Markdown(f\"Note that the `groups` column is empty, thats because these were created by an admin account from ex2\"))\n", "display(HTML(tabulate_endpoint(session_gollum.models, display_cols)))" ] }, { "cell_type": "markdown", "id": "6c2c0617-f99d-4d78-a10e-3ad4e1af613f", "metadata": {}, "source": [ "### 4.2.3 Analysis inherit groups from portfolios\n", "\n", "Analyses inherit the groups from its attached portfolio, but for a user to run the analysis it requires permission for both the **model** and **analysis**." ] }, { "cell_type": "code", "execution_count": null, "id": "7f4bf27a-4d5a-4c1d-991f-988e2719d328", "metadata": {}, "outputs": [], "source": [ "if not session_gandalf.portfolios.search({'name': 'lit_beacons'}).json():\n", " PORT_ID = session_gandalf.portfolios.create('lit_beacons').json()['id']\n", "else:\n", " PORT_ID = session_gandalf.portfolios.search({'name': 'lit_beacons'}).json().pop()['id']\n", "\n", "if not session_gandalf.analyses.search({'name': 'gondor_call_for_aid'}).json():\n", " session_gandalf.portfolios.location_file.post(PORT_ID, '-n/a-', content_type='text/csv')\n", " MODEL_ID = session_gandalf.models.search({'supplier_id': 'rohan'}).json().pop()['id']\n", " session_gandalf.analyses.create('gondor_call_for_aid', PORT_ID, MODEL_ID) \n", "\n", "display(Markdown(f\"### Gandalf's Portfolios\"))\n", "display(HTML(tabulate_endpoint(session_gandalf.portfolios, ['id', 'name', 'groups'])))\n", "\n", "\n", "display(Markdown(f\"### Gandalf's Analyses\"))\n", "display(HTML(tabulate_endpoint(session_gandalf.analyses, ['id', 'name', 'model', 'portfolio', 'status', 'groups'])))" ] }, { "cell_type": "markdown", "id": "60669088-ed7c-40ed-b1a6-347918e242c4", "metadata": {}, "source": [ "### 4.4 Removing access to a model. \n", "\n", "Frodo no longer needs access to the `rohan, support_response_time` model, so \n", "Gandalf edits the `groups` field and remove **the_shire** from the list. \n" ] }, { "cell_type": "code", "execution_count": null, "id": "325412ae-89c3-42ee-b3c6-75379ac28594", "metadata": {}, "outputs": [], "source": [ "rohan_model = session_gandalf.models.search({'supplier_id': 'rohan'}).json().pop()\n", "session_gandalf.models.patch(rohan_model['id'], {'groups': ['gondor']})\n", "\n", "display(Markdown(f\"### Frodo's CAT Models\"))\n", "display(Markdown(f\"Frodo's access to the rohan model was removed\"))\n", "display(HTML(tabulate_endpoint(session_frodo.models, display_cols)))\n", "\n", "\n", "display(Markdown(f\"### Frodo's Analyses\"))\n", "display(Markdown(f\"However can still see the analyses created before the model group removal\"))\n", "display(HTML(tabulate_endpoint(session_frodo.analyses, ['id', 'name', 'model', 'portfolio', 'status', 'groups'])))" ] }, { "cell_type": "markdown", "id": "93a2c29c-9117-48df-a91f-05fa5bdca6de", "metadata": {}, "source": [ "### 4.4.1 Users to both Models and Analyses to execute " ] }, { "cell_type": "markdown", "id": "72d91c35-e736-4178-8794-a21c9a2e7fbd", "metadata": {}, "source": [ "Frodo still has access to the analysis but not the model. " ] }, { "cell_type": "code", "execution_count": null, "id": "c4b72976-ecf6-47e7-9d75-0cf5b3a37bce", "metadata": {}, "outputs": [], "source": [ "ANALYSIS_ID = session_frodo.analyses.search({'name':'gondor_call_for_aid'}).json().pop()['id']\n", "\n", "try:\n", " r = session_frodo.analyses.generate(ANALYSIS_ID)\n", "except Exception as e: \n", " print(f'HTTP Error: {e.args[1].response.status_code}')\n", " print(f'Msg: {e.args[1].response.text}')" ] }, { "cell_type": "markdown", "id": "dd289e0b-773d-431b-846b-7ce6075f14d3", "metadata": {}, "source": [ "### 4.4.2 Users can only modify objects which have the same group.\n", "\n", "The user `sauron` wants to add **rohan** to the **mordor** group, but even with the ID the API returns 403" ] }, { "cell_type": "code", "execution_count": null, "id": "0ed83292-71dc-4df5-aac8-4e1a854932ac", "metadata": {}, "outputs": [], "source": [ "rohan_model = session_gandalf.models.search({'supplier_id': 'rohan'}).json().pop()\n", "r = session_sauron.models.patch(rohan_model['id'], {'groups': ['mordor']})\n", "\n", "print(f'HTTP Error: {r.status_code}')\n", "print(f'Msg: {r.text}')" ] } ], "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.4" } }, "nbformat": 4, "nbformat_minor": 5 }