From bbef8713cdbc35dbdeeac5b09275dc3fa3e969e5 Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Thu, 11 Apr 2024 11:05:00 -0300 Subject: [PATCH 1/3] Include users/groups to dask worker profiles --- .../stages/kubernetes_services/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/_nebari/stages/kubernetes_services/__init__.py b/src/_nebari/stages/kubernetes_services/__init__.py index 9c47fee6e..0071655d7 100644 --- a/src/_nebari/stages/kubernetes_services/__init__.py +++ b/src/_nebari/stages/kubernetes_services/__init__.py @@ -90,14 +90,10 @@ class Config: extra = "allow" -class JupyterLabProfile(schema.Base): +class ProfileAccess(schema.Base): access: AccessEnum = AccessEnum.all - display_name: str - description: str - default: bool = False users: typing.Optional[typing.List[str]] groups: typing.Optional[typing.List[str]] - kubespawner_override: typing.Optional[KubeSpawner] @pydantic.root_validator def only_yaml_can_have_groups_and_users(cls, values): @@ -112,7 +108,14 @@ def only_yaml_can_have_groups_and_users(cls, values): return values -class DaskWorkerProfile(schema.Base): +class JupyterLabProfile(ProfileAccess): + display_name: str + description: str + default: bool = False + kubespawner_override: typing.Optional[KubeSpawner] + + +class DaskWorkerProfile(ProfileAccess): worker_cores_limit: int worker_cores: int worker_memory_limit: str From 087b66a05ca4731cbe62c02e865f23b00efd5e6c Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Thu, 11 Apr 2024 11:05:23 -0300 Subject: [PATCH 2/3] Add permissions filters to daskgateway config --- .../dask-gateway/files/gateway_config.py | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py index 2219d14e5..22c8558da 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py @@ -81,6 +81,8 @@ async def authenticate(self, request): user.admin = "dask_gateway_admin" in data["roles"] user.groups = [Path(group).name for group in data["groups"]] + user.keycloak_profile_names = data.get("dask_profiles", []) + return user @@ -227,21 +229,60 @@ def base_username_mount(username, uid=1000, gid=100): } +def _sanitize_permissions(profile): + keys_to_remove = ["groups", "users", "access"] + + for key in keys_to_remove: + profile.pop(key, None) + + return profile + + def worker_profile(options, user): namespace, name = options.conda_environment.split("/") + return functools.reduce( deep_merge, [ base_node_group(options), base_conda_store_mounts(namespace, name), base_username_mount(user.name), - config["profiles"][options.profile], + _sanitize_permissions(config["profiles"][options.profile]), {"environment": {**options.environment_vars}}, ], {}, ) +def _filter_profiles(user, profiles): + """ + Filter access to profiles based on user's groups and username + """ + + def has_group_access(profile): + return not profile.get("groups") or set(user.groups).intersection( + profile["groups"] + ) + + def has_user_access(profile): + return not profile.get("users") or user.name in profile["users"] + + user_profiles = list(profiles.keys()) + + for name in list(user_profiles): + profile = profiles[name] + access_type = profile.get("access", "all") + + if access_type == "yaml": + if not (has_group_access(profile) and has_user_access(profile)): + user_profiles.remove(name) + elif access_type == "keycloak": + if name not in user.keycloak_profilenames: + user_profiles.remove(name) + + return user_profiles + + def user_options(user): default_namespace = config["default-conda-store-namespace"] allowed_namespaces = set( @@ -253,6 +294,8 @@ def user_options(user): continue conda_environments.append(f"{namespace}/{namespace}-{name}") + user_profiles = _filter_profiles(user, config["profiles"]) + args = [] if conda_environments: args += [ @@ -267,8 +310,8 @@ def user_options(user): args += [ Select( "profile", - list(config["profiles"].keys()), - default=list(config["profiles"].keys())[0], + user_profiles, + default=user_profiles[0], label="Cluster Profile", ) ] From 6cdd28f3887332ac84f781c532a7f42186b5a504 Mon Sep 17 00:00:00 2001 From: vinicius douglas cerutti Date: Thu, 11 Apr 2024 11:43:39 -0300 Subject: [PATCH 3/3] add profile attribute mapper --- .../dask-gateway/files/gateway_config.py | 1 + .../kubernetes/services/jupyterhub/main.tf | 1 + .../kubernetes/services/keycloak-client/main.tf | 17 +++++++++++++++++ .../services/keycloak-client/variables.tf | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py index 22c8558da..e96145b0b 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/dask-gateway/files/gateway_config.py @@ -277,6 +277,7 @@ def has_user_access(profile): if not (has_group_access(profile) and has_user_access(profile)): user_profiles.remove(name) elif access_type == "keycloak": + # Keycloak mapper should provide the 'daskworker_profiles' attribute from groups/user if name not in user.keycloak_profilenames: user_profiles.remove(name) diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf index e2ddf02f3..37d0257f0 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/jupyterhub/main.tf @@ -283,6 +283,7 @@ module "jupyterhub-openid-client" { var.jupyterhub-logout-redirect-url ] jupyterlab_profiles_mapper = true + daskworker_profiles_mapper = true } diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf index fd85eeb7a..ac95caf01 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/main.tf @@ -62,6 +62,23 @@ resource "keycloak_openid_user_attribute_protocol_mapper" "jupyterlab_profiles" aggregate_attributes = true } +resource "keycloak_openid_user_attribute_protocol_mapper" "daskworker_profiles" { + count = var.daskworker_profiles_mapper ? 1 : 0 + + realm_id = var.realm_id + client_id = keycloak_openid_client.main.id + name = "daskworker_profiles_mapper" + claim_name = "daskworker_profiles" + + add_to_id_token = true + add_to_access_token = true + add_to_userinfo = true + + user_attribute = "daskworker_profiles" + multivalued = true + aggregate_attributes = true +} + resource "keycloak_role" "main" { for_each = toset(flatten(values(var.role_mapping))) diff --git a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf index d20ecca48..c6f498b5c 100644 --- a/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf +++ b/src/_nebari/stages/kubernetes_services/template/modules/kubernetes/services/keycloak-client/variables.tf @@ -33,3 +33,9 @@ variable "jupyterlab_profiles_mapper" { type = bool default = false } + +variable "daskworker_profiles_mapper" { + description = "Create a mapper for daskworker_profiles group/user attributes" + type = bool + default = false +}