{
"cells": [
{
"cell_type": "markdown",
"id": "192d2500-9007-4775-ae00-1f3216637258",
"metadata": {},
"source": [
"\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",
"\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",
""
]
},
{
"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
}