From d0c6ca923b5beb696b93993dcbd856ec13a77d15 Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Tue, 24 Sep 2024 14:18:01 -0300 Subject: [PATCH 1/6] add compatibility prompt and notes for shared group mounting --- src/_nebari/upgrade.py | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/_nebari/upgrade.py b/src/_nebari/upgrade.py index ef8ecf7cf..875e89376 100644 --- a/src/_nebari/upgrade.py +++ b/src/_nebari/upgrade.py @@ -24,6 +24,7 @@ from typing_extensions import override from _nebari.config import backup_configuration +from _nebari.keycloak import get_keycloak_admin_from_config from _nebari.stages.infrastructure import ( provider_enum_default_node_groups_map, provider_enum_name_map, @@ -1235,6 +1236,62 @@ def _version_specific_upgrade( ) rich.print("") + rich.print("\n ⚠️ Warning ⚠️") + rich.print( + f"Nebari version [green]{self.version}[/green] introduces changes to how group directories are mounted in JupyterLab pods.\n\n" + "[bold]What does this mean?[/bold]\n" + "- Only groups with specific permissions will have their directories mounted.\n" + "- You need to confirm how Nebari should handle your groups.\n\n" + "[bold yellow]No data will be lost during this operation.[/bold yellow]\n" + "You can reverse this at any time by adding or removing the `allow-group-directory-creation-role` from your groups in the Keycloak UI.\n\n" + "For more information, please see the [green][link=https://www.nebari.dev/docs/how-tos/group-directory-creation]documentation[/link][/green].\n" + ) + + # Prompt the user for action + confirm = Prompt.ask( + "[bold]Would you like Nebari to update your group permissions now?[/bold] (y/n)", + choices=["y", "n"], + default="y", + ) + + if confirm.lower() == "y": + # Proceed with updating group permissions + keycloak_admin = get_keycloak_admin_from_config(config) + groups = keycloak_admin.get_groups() + groups_without_role = [ + group + for group in groups + if "allow-group-directory-creation-role" + not in group.get("attributes", {}) + ] + if groups_without_role: + group_names = ", ".join( + [group["name"] for group in groups_without_role] + ) + rich.print( + f"\n[bold]Updating the following groups with the required permissions:[/bold] {group_names}\n" + ) + for group in groups_without_role: + _group_id = group["id"] + keycloak_admin.assign_group_client_roles( + group_id=_group_id, + client_id="jupyterhub", + roles=["allow-group-directory-creation-role"], + ) + rich.print( + "[green]Group permissions have been updated successfully.[/green]" + ) + else: + rich.print( + "\n[green]All groups already have the required permissions.[/green]\n" + ) + else: + rich.print( + "\n[bold yellow]You have chosen not to update group permissions at this time.[/bold yellow]" + ) + rich.print( + "You can update them later by visiting the Keycloak UI and adding or removing the `allow-group-directory-creation-role` from your groups.\n" + ) return config From e45e5852b5f52a84de6902032cb88e5e00a95fb3 Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Tue, 24 Sep 2024 17:59:31 -0300 Subject: [PATCH 2/6] update upgrade message & default prompt to no --- src/_nebari/upgrade.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/_nebari/upgrade.py b/src/_nebari/upgrade.py index 875e89376..56cb565f0 100644 --- a/src/_nebari/upgrade.py +++ b/src/_nebari/upgrade.py @@ -1236,22 +1236,29 @@ def _version_specific_upgrade( ) rich.print("") - rich.print("\n ⚠️ Warning ⚠️") - rich.print( - f"Nebari version [green]{self.version}[/green] introduces changes to how group directories are mounted in JupyterLab pods.\n\n" - "[bold]What does this mean?[/bold]\n" - "- Only groups with specific permissions will have their directories mounted.\n" - "- You need to confirm how Nebari should handle your groups.\n\n" - "[bold yellow]No data will be lost during this operation.[/bold yellow]\n" - "You can reverse this at any time by adding or removing the `allow-group-directory-creation-role` from your groups in the Keycloak UI.\n\n" - "For more information, please see the [green][link=https://www.nebari.dev/docs/how-tos/group-directory-creation]documentation[/link][/green].\n" + rich.print("\n ⚠️ Upgrade Warning ⚠️") + + text = textwrap.dedent( + """ + Nebari version [green]2024.9.1[/green] introduces changes to how group + directories are mounted in JupyterLab pods, now only groups with specific + permissions will have their directories mounted. + + You will be asked to confirm how Nebari should handle your groups. + No data will be lost during this operation. You can reverse this at any time + by adding or removing the `allow-group-directory-creation-role` from your + groups in the Keycloak UI. + + + For more information, please see the [green][link=https://www.nebari.dev/docs/how-tos/group-directory-creation]documentation[/link][/green]. + """ ) + rich.print(text) - # Prompt the user for action confirm = Prompt.ask( "[bold]Would you like Nebari to update your group permissions now?[/bold] (y/n)", - choices=["y", "n"], - default="y", + choices=["y", "N"], + default="N", ) if confirm.lower() == "y": From 329e902b46034f3561449cc0e49be5caf4969628 Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Tue, 24 Sep 2024 19:01:02 -0300 Subject: [PATCH 3/6] fix failing ci test --- src/_nebari/keycloak.py | 41 +++++++++++++++++++------------- src/_nebari/upgrade.py | 28 +++++++++++++++------- tests/tests_unit/test_upgrade.py | 5 ++++ 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/_nebari/keycloak.py b/src/_nebari/keycloak.py index ea8815940..6bfea9b8b 100644 --- a/src/_nebari/keycloak.py +++ b/src/_nebari/keycloak.py @@ -81,27 +81,16 @@ def list_users(keycloak_admin: keycloak.KeycloakAdmin): ) -def get_keycloak_admin_from_config(config: schema.Main): - keycloak_server_url = os.environ.get( - "KEYCLOAK_SERVER_URL", f"https://{config.domain}/auth/" - ) - - keycloak_username = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "root") - keycloak_password = os.environ.get( - "KEYCLOAK_ADMIN_PASSWORD", config.security.keycloak.initial_root_password - ) - - should_verify_tls = config.certificate.type != CertificateEnum.selfsigned - +def get_keycloak_admin(server_url, username, password, verify=False): try: keycloak_admin = keycloak.KeycloakAdmin( - server_url=keycloak_server_url, - username=keycloak_username, - password=keycloak_password, + server_url=server_url, + username=username, + password=password, realm_name=os.environ.get("KEYCLOAK_REALM", "nebari"), user_realm_name="master", auto_refresh_token=("get", "put", "post", "delete"), - verify=should_verify_tls, + verify=verify, ) except ( keycloak.exceptions.KeycloakConnectionError, @@ -112,6 +101,26 @@ def get_keycloak_admin_from_config(config: schema.Main): return keycloak_admin +def get_keycloak_admin_from_config(config: schema.Main): + keycloak_server_url = os.environ.get( + "KEYCLOAK_SERVER_URL", f"https://{config.domain}/auth/" + ) + + keycloak_username = os.environ.get("KEYCLOAK_ADMIN_USERNAME", "root") + keycloak_password = os.environ.get( + "KEYCLOAK_ADMIN_PASSWORD", config.security.keycloak.initial_root_password + ) + + should_verify_tls = config.certificate.type != CertificateEnum.selfsigned + + return get_keycloak_admin( + server_url=keycloak_server_url, + username=keycloak_username, + password=keycloak_password, + verify=should_verify_tls, + ) + + def keycloak_rest_api_call(config: schema.Main = None, request: str = None): """Communicate directly with the Keycloak REST API by passing it a request""" keycloak_server_url = os.environ.get( diff --git a/src/_nebari/upgrade.py b/src/_nebari/upgrade.py index 56cb565f0..bcc08be0f 100644 --- a/src/_nebari/upgrade.py +++ b/src/_nebari/upgrade.py @@ -24,7 +24,7 @@ from typing_extensions import override from _nebari.config import backup_configuration -from _nebari.keycloak import get_keycloak_admin_from_config +from _nebari.keycloak import get_keycloak_admin from _nebari.stages.infrastructure import ( provider_enum_default_node_groups_map, provider_enum_name_map, @@ -1256,21 +1256,33 @@ def _version_specific_upgrade( rich.print(text) confirm = Prompt.ask( - "[bold]Would you like Nebari to update your group permissions now?[/bold] (y/n)", + "[bold]Would you like Nebari to update your group permissions now?[/bold]", choices=["y", "N"], default="N", ) - if confirm.lower() == "y": # Proceed with updating group permissions - keycloak_admin = get_keycloak_admin_from_config(config) + keycloak_admin = get_keycloak_admin( + server_url=f"https://{config['domain']}/auth/", + username="root", + password=config["security"]["keycloak"]["initial_root_password"], + ) + client_id = keycloak_admin.get_client_id("jupyterhub") + _role_representation = keycloak_admin.get_role_by_id( + role_id=keycloak_admin.get_client_role_id( + client_id=client_id, role_name="allow-group-directory-creation-role" + ) + ) groups = keycloak_admin.get_groups() + groups_with_roles = keycloak_admin.get_client_role_groups( + client_id=client_id, role_name="allow-group-directory-creation-role" + ) groups_without_role = [ group for group in groups - if "allow-group-directory-creation-role" - not in group.get("attributes", {}) + if group["id"] not in [group["id"] for group in groups_with_roles] ] + if groups_without_role: group_names = ", ".join( [group["name"] for group in groups_without_role] @@ -1282,8 +1294,8 @@ def _version_specific_upgrade( _group_id = group["id"] keycloak_admin.assign_group_client_roles( group_id=_group_id, - client_id="jupyterhub", - roles=["allow-group-directory-creation-role"], + client_id=client_id, + roles=[_role_representation], ) rich.print( "[green]Group permissions have been updated successfully.[/green]" diff --git a/tests/tests_unit/test_upgrade.py b/tests/tests_unit/test_upgrade.py index a19095726..d328c74e9 100644 --- a/tests/tests_unit/test_upgrade.py +++ b/tests/tests_unit/test_upgrade.py @@ -67,6 +67,11 @@ def mock_input(prompt, **kwargs): == "Have you backed up your custom dashboards (if necessary), deleted the prometheus-node-exporter daemonset and updated the kube-prometheus-stack CRDs?" ): return "y" + elif ( + prompt + == "[bold]Would you like Nebari to update your group permissions now?[/bold]" + ): + return "N" # All other prompts will be answered with "y" else: return "y" From 984a01298377b9bf0a68d1356152772a7c7a98fc Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Wed, 25 Sep 2024 16:27:45 -0300 Subject: [PATCH 4/6] add review suggestions --- src/_nebari/upgrade.py | 93 ++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/src/_nebari/upgrade.py b/src/_nebari/upgrade.py index bcc08be0f..8ac556d55 100644 --- a/src/_nebari/upgrade.py +++ b/src/_nebari/upgrade.py @@ -1240,77 +1240,92 @@ def _version_specific_upgrade( text = textwrap.dedent( """ - Nebari version [green]2024.9.1[/green] introduces changes to how group - directories are mounted in JupyterLab pods, now only groups with specific - permissions will have their directories mounted. + Nebari [green]2024.9.1[/green] introduces changes to how group + directories are mounted in JupyterLab pods. - You will be asked to confirm how Nebari should handle your groups. - No data will be lost during this operation. You can reverse this at any time - by adding or removing the `allow-group-directory-creation-role` from your - groups in the Keycloak UI. + Previously, every Keycloak group in the Nebari realm automatically created a + shared directory at ~/shared/, accessible to all group members + in their JupyterLab pods. + Starting with Nebari [green]2024.9.1[/green], only groups assigned the + JupyterHub client role [magenta]allow-group-directory-creation[/magenta] will have their + directories mounted. - For more information, please see the [green][link=https://www.nebari.dev/docs/how-tos/group-directory-creation]documentation[/link][/green]. + By default, the admin, analyst, and developer groups will have this + role assigned during the upgrade. For other groups, you'll now need to + assign this role manually in the Keycloak UI to have their directories + mounted. + + After the upgrade, users with active sessions may experience issues with + their shared folders due to cached user data. To resolve this, all users + will need to log out and log back in to update their session data. + + For more details check our [green][link=https://www.nebari.dev/docs/references/release/]release notes[/link][/green]. """ ) rich.print(text) + keycloak_admin = None - confirm = Prompt.ask( - "[bold]Would you like Nebari to update your group permissions now?[/bold]", - choices=["y", "N"], - default="N", + # Prompt the user for role assignment (if yes, transforms the response into bool) + assign_roles = ( + Prompt.ask( + "[bold]Would you like Nebari to assign the corresponding role to all of your current groups automatically?[/bold]", + choices=["y", "N"], + default="N", + ).lower() + == "y" ) - if confirm.lower() == "y": - # Proceed with updating group permissions + + if assign_roles: + # In case this is done with a local deployment + import urllib3 + + urllib3.disable_warnings() + keycloak_admin = get_keycloak_admin( server_url=f"https://{config['domain']}/auth/", username="root", password=config["security"]["keycloak"]["initial_root_password"], ) + + # Proceed with updating group permissions client_id = keycloak_admin.get_client_id("jupyterhub") - _role_representation = keycloak_admin.get_role_by_id( - role_id=keycloak_admin.get_client_role_id( - client_id=client_id, role_name="allow-group-directory-creation-role" - ) + role_name = "allow-group-directory-creation-role" + role_id = keycloak_admin.get_client_role_id( + client_id=client_id, role_name=role_name ) - groups = keycloak_admin.get_groups() - groups_with_roles = keycloak_admin.get_client_role_groups( - client_id=client_id, role_name="allow-group-directory-creation-role" + role_representation = keycloak_admin.get_role_by_id(role_id=role_id) + + # Fetch all groups and groups with the role + all_groups = keycloak_admin.get_groups() + groups_with_role = keycloak_admin.get_client_role_groups( + client_id=client_id, role_name=role_name ) + groups_with_role_ids = {group["id"] for group in groups_with_role} + + # Identify groups without the role groups_without_role = [ - group - for group in groups - if group["id"] not in [group["id"] for group in groups_with_roles] + group for group in all_groups if group["id"] not in groups_with_role_ids ] if groups_without_role: - group_names = ", ".join( - [group["name"] for group in groups_without_role] - ) + group_names = ", ".join(group["name"] for group in groups_without_role) rich.print( f"\n[bold]Updating the following groups with the required permissions:[/bold] {group_names}\n" ) for group in groups_without_role: - _group_id = group["id"] keycloak_admin.assign_group_client_roles( - group_id=_group_id, + group_id=group["id"], client_id=client_id, - roles=[_role_representation], + roles=[role_representation], ) rich.print( - "[green]Group permissions have been updated successfully.[/green]" + "\n[green]Group permissions have been updated successfully.[/green]" ) else: rich.print( - "\n[green]All groups already have the required permissions.[/green]\n" + "\n[green]All groups already have the required permissions.[/green]" ) - else: - rich.print( - "\n[bold yellow]You have chosen not to update group permissions at this time.[/bold yellow]" - ) - rich.print( - "You can update them later by visiting the Keycloak UI and adding or removing the `allow-group-directory-creation-role` from your groups.\n" - ) return config From e6203d57c4b687f69c9926466df5263c549d81ad Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Wed, 25 Sep 2024 16:30:04 -0300 Subject: [PATCH 5/6] update unit test exception --- tests/tests_unit/test_upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests_unit/test_upgrade.py b/tests/tests_unit/test_upgrade.py index d328c74e9..f6e3f8034 100644 --- a/tests/tests_unit/test_upgrade.py +++ b/tests/tests_unit/test_upgrade.py @@ -69,7 +69,7 @@ def mock_input(prompt, **kwargs): return "y" elif ( prompt - == "[bold]Would you like Nebari to update your group permissions now?[/bold]" + == "[bold]Would you like Nebari to assign the corresponding role to all of your current groups automatically?[/bold]" ): return "N" # All other prompts will be answered with "y" From 61dae66be58747d8c5ad54414d69917d38bf686f Mon Sep 17 00:00:00 2001 From: viniciusdc Date: Fri, 27 Sep 2024 10:14:53 -0300 Subject: [PATCH 6/6] leave note for logging out users --- src/_nebari/upgrade.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/_nebari/upgrade.py b/src/_nebari/upgrade.py index 8ac556d55..6536612f2 100644 --- a/src/_nebari/upgrade.py +++ b/src/_nebari/upgrade.py @@ -1240,6 +1240,9 @@ def _version_specific_upgrade( text = textwrap.dedent( """ + Please ensure no users are currently logged in prior to deploying this + update. + Nebari [green]2024.9.1[/green] introduces changes to how group directories are mounted in JupyterLab pods. @@ -1256,10 +1259,6 @@ def _version_specific_upgrade( assign this role manually in the Keycloak UI to have their directories mounted. - After the upgrade, users with active sessions may experience issues with - their shared folders due to cached user data. To resolve this, all users - will need to log out and log back in to update their session data. - For more details check our [green][link=https://www.nebari.dev/docs/references/release/]release notes[/link][/green]. """ )