diff --git a/config/clusters/2i2c-aws-us/itcoocean.values.yaml b/config/clusters/2i2c-aws-us/itcoocean.values.yaml index e4ca3a652..8a4f38feb 100644 --- a/config/clusters/2i2c-aws-us/itcoocean.values.yaml +++ b/config/clusters/2i2c-aws-us/itcoocean.values.yaml @@ -248,7 +248,7 @@ jupyterhub: - display_name: "Bring your own image" description: Specify your own docker image (must have python and jupyterhub installed in it) slug: custom - allowed_teams: + allowed_groups: - Hackweek-ITCOocean:itcoocean-hackweek-2023 - nmfs-opensci:2i2c-demo - 2i2c-org:hub-access-for-2i2c-staff diff --git a/config/clusters/2i2c-aws-us/showcase.values.yaml b/config/clusters/2i2c-aws-us/showcase.values.yaml index 8809eff4d..ef0d1d064 100644 --- a/config/clusters/2i2c-aws-us/showcase.values.yaml +++ b/config/clusters/2i2c-aws-us/showcase.values.yaml @@ -58,7 +58,7 @@ basehub: profileList: - display_name: "Magic Link Demo" description: "For demoing magic links" - allowed_teams: + allowed_groups: - 2i2c-community-showcase:magiclinks-demo kubespawner_override: image: pangeo/pangeo-notebook:2023.06.20 @@ -69,7 +69,7 @@ basehub: node.kubernetes.io/instance-type: r5.xlarge - display_name: "NASA TOPS-T ScienceCore-ClimateRisk" description: "For collaborative work on 2i2c/MD's NASA TOPS-T ScienceCore Module" - allowed_teams: + allowed_groups: - 2i2c-demo-hub-access:showcase-topst - 2i2c-org:hub-access-for-2i2c-staff - ScienceCore:climaterisk-team @@ -82,7 +82,7 @@ basehub: node.kubernetes.io/instance-type: r5.xlarge - display_name: "NASA TOPS-T ScienceCore" description: "JupyterHubs for NASA ScienceCore Modules" - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - ScienceCore:2i2c-showcase profile_options: @@ -132,7 +132,7 @@ basehub: node.kubernetes.io/instance-type: r5.xlarge - display_name: "Shared Small: 1-4 CPU, 8-32 GB" description: "A shared machine, the recommended option until you experience a limitation." - allowed_teams: &allowed_teams + allowed_groups: &allowed_groups - 2i2c-org:hub-access-for-2i2c-staff - 2i2c-community-showcase:access-2i2c-showcase profile_options: &profile_options @@ -188,7 +188,7 @@ basehub: - display_name: "Small: 4 CPU, 32 GB" description: "A dedicated machine for you." profile_options: *profile_options - allowed_teams: *allowed_teams + allowed_groups: *allowed_groups kubespawner_override: mem_guarantee: 28.937G cpu_guarantee: 0.4 @@ -199,7 +199,7 @@ basehub: - display_name: "Medium: 16 CPU, 128 GB" description: "A dedicated machine for you." profile_options: *profile_options - allowed_teams: *allowed_teams + allowed_groups: *allowed_groups kubespawner_override: mem_guarantee: 120.513G cpu_guarantee: 1.6 @@ -210,7 +210,7 @@ basehub: - display_name: "Large: 64 CPU, 512 GB" description: "A dedicated machine for you" profile_options: *profile_options - allowed_teams: *allowed_teams + allowed_groups: *allowed_groups kubespawner_override: mem_guarantee: 489.13G cpu_guarantee: 6.4 @@ -220,7 +220,7 @@ basehub: - display_name: NVIDIA Tesla T4, ~16 GB, ~4 CPUs slug: gpu - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff description: "Start a container on a dedicated node with a GPU" profile_options: diff --git a/config/clusters/earthscope/common.values.yaml b/config/clusters/earthscope/common.values.yaml index 2021e3bbe..dc9015f7f 100644 --- a/config/clusters/earthscope/common.values.yaml +++ b/config/clusters/earthscope/common.values.yaml @@ -37,11 +37,10 @@ basehub: hub: extraConfig: 001-username-claim: | - from oauthenticator.auth0 import Auth0OAuthenticator - from traitlets import List, Unicode, default - from urllib.parse import urlencode + from oauthenticator.generic import GenericOAuthenticator + from traitlets import List, Unicode - class CustomAuth0OAuthenticator(Auth0OAuthenticator): + class CustomGenericOAuthenticator(GenericOAuthenticator): # required_scopes functionality comes in from https://github.com/jupyterhub/oauthenticator/pull/719 # Can be removed from here once that PR is merged required_scopes = List( @@ -62,28 +61,6 @@ basehub: """, ) - # Upstreamed at https://github.com/jupyterhub/oauthenticator/pull/722 - logout_redirect_to_url = Unicode( - config=True, - help=""" - Redirect to this URL after the user is logged out. - - Must be explicitly added to the "Allowed Logout URLs" in the configuration - for this Auth0 application. See https://auth0.com/docs/authenticate/login/logout/redirect-users-after-logout - for more information. - """ - ) - - @default("logout_redirect_url") - def _logout_redirect_url_default(self): - url = f"https://{self.auth0_domain}/v2/logout" - if self.logout_redirect_to_url: - # If a redirectTo is set, we must also include the `client_id` - # Auth0 expects `client_id` to be snake cased while `redirectTo` is camel cased - params = urlencode({"client_id": self.client_id, "redirectTo": self.logout_redirect_to_url}) - url = f"{url}?{params}" - return url - async def check_allowed(self, username, auth_model): if await super().check_allowed(username, auth_model): return True @@ -99,6 +76,14 @@ basehub: return False + async def authenticate(self, *args, **kwargs): + resp = await super().authenticate(*args, **kwargs) + # Setup groups to be same as list of scopes granted + # This can go away after https://github.com/jupyterhub/oauthenticator/pull/735 is + # merged + resp["groups"] = resp["auth_state"]["scope"] + return resp + def populate_token(spawner, auth_state): # For our deployment-service-check health check user, there is no auth_state. # So these env variables need not be set. @@ -112,15 +97,13 @@ basehub: c.Spawner.auth_state_hook = populate_token - c.JupyterHub.authenticator_class = CustomAuth0OAuthenticator + c.JupyterHub.authenticator_class = CustomGenericOAuthenticator config: - JupyterHub: - authenticator_class: auth0 - CustomAuth0OAuthenticator: + CustomGenericOAuthenticator: required_scopes: # This allows EarthScope to control who can login to the hub - geolab - Auth0OAuthenticator: + GenericOAuthenticator: scope: - openid # This gives us refresh token @@ -130,10 +113,14 @@ basehub: # automatically granted this scope, so we can test. See # https://2i2c.freshdesk.com/a/tickets/1280 for how this was granted. - geolab + - geolab:dev + - geolab:power extra_authorize_params: # This isn't an actual URL, just a string. Must not have a trailing slash audience: https://api.dev.earthscope.org username_claim: sub + # Convert 'scope' from the OAuth2 response into JupyterHub groups + manage_groups: true CILogonOAuthenticator: allowed_idps: http://github.com/login/oauth/authorize: @@ -152,6 +139,10 @@ basehub: profileList: - display_name: "Shared Small: 1-4 CPU, 8-32 GB" description: "A shared machine, the recommended option until you experience a limitation." + allowed_groups: + - geolab + - geolab:dev + - geolab:power profile_options: &profile_options image: display_name: Image @@ -186,9 +177,12 @@ basehub: mem_limit: null node_selector: node.kubernetes.io/instance-type: r5.xlarge - - display_name: "Small: 4 CPU, 32 GB" description: "A dedicated machine for you." + allowed_groups: + - geolab + - geolab:dev + - geolab:power profile_options: *profile_options kubespawner_override: mem_guarantee: 28.937G @@ -196,20 +190,23 @@ basehub: mem_limit: null node_selector: node.kubernetes.io/instance-type: r5.xlarge - - display_name: "Medium: 16 CPU, 128 GB" description: "A dedicated machine for you." profile_options: *profile_options + allowed_groups: + - geolab:dev + - geolab:power kubespawner_override: mem_guarantee: 120.513G cpu_guarantee: 1.6 mem_limit: null node_selector: node.kubernetes.io/instance-type: r5.4xlarge - - display_name: "Large: 64 CPU, 512 GB" description: "A dedicated machine for you" profile_options: *profile_options + allowed_groups: + - geolab:power kubespawner_override: mem_guarantee: 489.13G cpu_guarantee: 6.4 diff --git a/config/clusters/earthscope/enc-prod.secret.values.yaml b/config/clusters/earthscope/enc-prod.secret.values.yaml index 0300bd385..7ec0eabf0 100644 --- a/config/clusters/earthscope/enc-prod.secret.values.yaml +++ b/config/clusters/earthscope/enc-prod.secret.values.yaml @@ -2,9 +2,9 @@ basehub: jupyterhub: hub: config: - Auth0OAuthenticator: - client_id: ENC[AES256_GCM,data:qn8Xel6vzFKHuL7gP8aGKQr3C7AGORQ7sCyNvKulbDE=,iv:bWYt/w31HcaEDjUBW3DZv/Lb4Ny/BPEjoBTsjp0XP6g=,tag:/02E1lYfhfOMcd+P2+DV8Q==,type:str] - client_secret: ENC[AES256_GCM,data:qry2vIkYLTRd7rlg6RTO6pB+e4SP5mvzClqagyJbbzXYkdeiGQccVFsERQ15RT/BRDX/PX4Bj5ZxcuCY9wGsxw==,iv:k763ow53AuqWG7dSyqkaosa9O4NwufRnmmORRxssGQA=,tag:MX4zqeOS3vdxhhYUljipJA==,type:str] + GenericOAuthenticator: + client_id: ENC[AES256_GCM,data:+ctWM1MpyksEjMLTnVZAw+N0Wv6ZNXL+fHdeamt64Ow=,iv:1KBoaNQTaUmyAt1wAO9pmvOkoLCl+B2eCBIu3SsRKYA=,tag:sK3EmbdSJP3UldX51274xA==,type:str] + client_secret: ENC[AES256_GCM,data:UtnmnF84dQ50h741JNvLmfBkzoI6ui16YVV8tRh1GXyCIrcu5bgy5StRIIjw3uRVo7g7bFFdGN6lIeQIUVd+Pw==,iv:arvQ5RbKiHFFNdyksmIA9UVoHWRdXgeKAhDGPPE7qrU=,tag:2NdZqXXBpEIUhrA3oTnKPw==,type:str] sops: kms: [] gcp_kms: @@ -14,8 +14,8 @@ sops: azure_kv: [] hc_vault: [] age: [] - lastmodified: "2024-02-01T19:48:36Z" - mac: ENC[AES256_GCM,data:Cw3rTUGqQlymWXXu/Z7qLSAIlULn5B3SAPxbzkeBDCFSO8u4fhuZXEjoEBvFFdujdEtU9Q7bASKRyl4aveZDJ+aZHboKNDV77d7atONojcEFj/DIy2ELQriMwyq1hx5hZS/onGgt8XLmcjXDJdMH6zEZOYrZl93uTuoS+Qt+4GI=,iv:oCWMr+17mgo+P1btrLglokBO3yYZ9JpZBTx36Vhtb3s=,tag:GEN4T0/9KQzz4+oTxvhbBQ==,type:str] + lastmodified: "2024-03-29T23:50:57Z" + mac: ENC[AES256_GCM,data:h9pUWffgf8vBqG4timmCMharFGj1jdP8iSaaczx1GfzouUG+hhlG82OQTFVSmLwhHkzlmxJxw+t7gi6Zwx9nNgVVfnwa4Qhw6V/XWrBRr8gre2I9+MuXXeYOcjiDqIyasF0TYxGW/kvLZ6+khGvi4iIhnk9rJOk/LpFhpj7IthQ=,iv:1yIFhnW1Mv+d5bBKFGgpMDCCt5zPGfP9YekGey4KF/g=,tag:YgXBpr03lP5q9y1sJ2CsUA==,type:str] pgp: [] unencrypted_suffix: _unencrypted - version: 3.7.3 + version: 3.8.1 diff --git a/config/clusters/earthscope/enc-staging.secret.values.yaml b/config/clusters/earthscope/enc-staging.secret.values.yaml index 9ae205942..f48a3830a 100644 --- a/config/clusters/earthscope/enc-staging.secret.values.yaml +++ b/config/clusters/earthscope/enc-staging.secret.values.yaml @@ -2,9 +2,9 @@ basehub: jupyterhub: hub: config: - Auth0OAuthenticator: - client_id: ENC[AES256_GCM,data:urLrYypX6IUSVpqFAumEAi9aGJKyQv8oQuNqw5HNhKo=,iv:sQcq2R5wbS2P00nygxPQ3p2LdAsxkRQrk4jvnMWAjQg=,tag:eosLpXx6vWQMNjIxDcsC7Q==,type:str] - client_secret: ENC[AES256_GCM,data:PrphM7gVSfUOInO008VgfhNU4r1+I4oLRT+ypJv5848Bvy1nN+ARzTrPgu7Q3KIiCaXyfNd3Xv6ieb0lsKCLZw==,iv:Vnbo4jG0sARtOL28GxgGAKKITQb5Tx6/TNscWUNgkJU=,tag:RgxhqVzdfsmwMTTEMCn2Zw==,type:str] + GenericOAuthenticator: + client_id: ENC[AES256_GCM,data:Rpa6XhJLmHBkccOZM58T0IwcviJvc2+jbLbL3LDQxgI=,iv:57//hbKbkT8PDa1kanOoS4wlWLvc1hp8fyGgMMaUKzk=,tag:zyv29aa/M7cqar2izZDRTg==,type:str] + client_secret: ENC[AES256_GCM,data:w7feSVDwFN0mbxvLH1DEpw/eanx5+vJXZ7JPSTkVxIAm0aZod4H7lhlEy/gmMgPUJfBF32tXPrrYh6Z5E83oIQ==,iv:RQt6NCiDwAwn15XGxF7T+DVdYck0kw/hKEV9ULgxY1k=,tag:BRnwMh76yaMF7RyKPhBd/g==,type:str] sops: kms: [] gcp_kms: @@ -14,8 +14,8 @@ sops: azure_kv: [] hc_vault: [] age: [] - lastmodified: "2024-02-01T19:48:31Z" - mac: ENC[AES256_GCM,data:ZYVgv+u0FD+jxYtgyITNLXr5bHNEEkkXtTM0SJGv8txbAVM4yt1k9CF95iVevPsRGG2yztY5vTDQaFGeg0tLGmG55fuuliZhMrB9RsDkmM3qEibVgQQTQZI5ZUciWHSBGm/NCMKnj6ujIx0h3E3cjtZBESIpONH+66kbuGhAlMo=,iv:ORwgid7PCff05bxWN9FuWNCN+wLY+bVZi0GfGDZwQj4=,tag:O+Bkh0UbeDzmAvYWJ5PpKQ==,type:str] + lastmodified: "2024-03-29T23:43:10Z" + mac: ENC[AES256_GCM,data:OnvUNbNHox7iF98w1aJSnrFJ1C3FSD+dz/l7ZK1z5uBnJAyhX3FhVoDGmA3TWAtS5U+ebiz8RbbVjJ/ge687ke2dL/Lnd9Ueay2tsF4ac1BYF6i5LqqsHqzaPwkrRVazB1aRgKx/O37Plm8KuAg2o9dN8jGtjnnSlbIxgJuJIUQ=,iv:Eo0iQ6qrbbcUkPFHzBwuMBGi1fCYnVWLPkkn9GQfrig=,tag:iUbwcWtat7qkfNE5i5MU6A==,type:str] pgp: [] unencrypted_suffix: _unencrypted - version: 3.7.3 + version: 3.8.1 diff --git a/config/clusters/earthscope/prod.values.yaml b/config/clusters/earthscope/prod.values.yaml index 55c932cbe..8aec56439 100644 --- a/config/clusters/earthscope/prod.values.yaml +++ b/config/clusters/earthscope/prod.values.yaml @@ -12,10 +12,11 @@ basehub: name: "EarthScope" hub: config: - CustomAuth0OAuthenticator: - logout_redirect_to_url: https://geolab.earthscope.cloud - Auth0OAuthenticator: - auth0_domain: login.earthscope.org + GenericOAuthenticator: + token_url: https://login.earthscope.org/oauth/token + authorize_url: https://login.earthscope.org/authorize + userdata_url: https://login.earthscope.org/userinfo + logout_redirect_url: https://login.earthscope.org/v2/logout?client_id=2PbhUTbRU6e7uIaaEZIShotx15MbvsJJ extra_authorize_params: # This isn't an actual URL, just a string. Must not have a trailing slash audience: https://api.earthscope.org diff --git a/config/clusters/earthscope/staging.values.yaml b/config/clusters/earthscope/staging.values.yaml index 11541d14a..91c420e16 100644 --- a/config/clusters/earthscope/staging.values.yaml +++ b/config/clusters/earthscope/staging.values.yaml @@ -13,10 +13,11 @@ basehub: name: "EarthScope staging" hub: config: - CustomAuth0OAuthenticator: - logout_redirect_to_url: https://staging.geolab.earthscope.cloud - Auth0OAuthenticator: - auth0_domain: login-dev.earthscope.org + GenericOAuthenticator: + token_url: https://login-dev.earthscope.org/oauth/token + authorize_url: https://login-dev.earthscope.org/authorize + userdata_url: https://login-dev.earthscope.org/userinfo + logout_redirect_url: https://login-dev.earthscope.org/v2/logout?client_id=Kn6kSKtw9TqgrSrEmDS0rlBM7Sc69BkL extra_authorize_params: # This isn't an actual URL, just a string. Must not have a trailing slash audience: https://api.dev.earthscope.org diff --git a/config/clusters/leap/common.values.yaml b/config/clusters/leap/common.values.yaml index 44536212a..109016591 100644 --- a/config/clusters/leap/common.values.yaml +++ b/config/clusters/leap/common.values.yaml @@ -132,7 +132,7 @@ basehub: description: &profile_list_description "Start a container limited to a chosen share of capacity on a node of this type" slug: medium-full default: true - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - leap-stc:leap-pangeo-full-access profile_options: @@ -235,13 +235,13 @@ basehub: # NOTE: This is the second medium profile list entry, with less node # share options for a different subset of users via the basehub - # specific allowed_teams configuration. + # specific allowed_groups configuration. # - display_name: "CPU only" description: *profile_list_description slug: medium-base default: true - allowed_teams: + allowed_groups: - leap-stc:leap-pangeo-base-access profile_options: requests: @@ -262,7 +262,7 @@ basehub: - display_name: GPU slug: gpu description: NVIDIA Tesla T4, 24GB RAM, 8 CPUs - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - leap-stc:leap-pangeo-full-access profile_options: diff --git a/config/clusters/meom-ige/common.values.yaml b/config/clusters/meom-ige/common.values.yaml index f1edae137..05fd09557 100644 --- a/config/clusters/meom-ige/common.values.yaml +++ b/config/clusters/meom-ige/common.values.yaml @@ -45,7 +45,7 @@ basehub: # # - display_name: Grenoble demo # default: true - # allowed_teams: + # allowed_groups: # - 2i2c-org:hub-access-for-2i2c-staff # - meom-group:hub-users # long term users # - demo-dask-grenoble2023:demo # temporary users for event @@ -93,7 +93,7 @@ basehub: # RAM on a node, not total node capacity - display_name: "Small" default: true - allowed_teams: &allowed_teams_normal_use + allowed_groups: &allowed_groups_normal_use - 2i2c-org:hub-access-for-2i2c-staff - meom-group:hub-users # long term users description: "~2 CPU, ~8G RAM" @@ -103,7 +103,7 @@ basehub: node_selector: node.kubernetes.io/instance-type: n1-standard-2 - display_name: "Medium" - allowed_teams: *allowed_teams_normal_use + allowed_groups: *allowed_groups_normal_use description: "~8 CPU, ~32G RAM" kubespawner_override: mem_limit: 32G @@ -111,7 +111,7 @@ basehub: node_selector: node.kubernetes.io/instance-type: n1-standard-8 - display_name: "Large" - allowed_teams: *allowed_teams_normal_use + allowed_groups: *allowed_groups_normal_use description: "~16 CPU, ~64G RAM" kubespawner_override: mem_limit: 64G @@ -119,7 +119,7 @@ basehub: node_selector: node.kubernetes.io/instance-type: n1-standard-16 - display_name: "Very Large" - allowed_teams: *allowed_teams_normal_use + allowed_groups: *allowed_groups_normal_use description: "~32 CPU, ~128G RAM" kubespawner_override: mem_limit: 128G @@ -127,7 +127,7 @@ basehub: node_selector: node.kubernetes.io/instance-type: n1-standard-32 - display_name: "Huge" - allowed_teams: *allowed_teams_normal_use + allowed_groups: *allowed_groups_normal_use description: "~64 CPU, ~256G RAM" kubespawner_override: mem_limit: 256G diff --git a/config/clusters/nasa-cryo/common.values.yaml b/config/clusters/nasa-cryo/common.values.yaml index 1f45fffe3..c0b578a14 100644 --- a/config/clusters/nasa-cryo/common.values.yaml +++ b/config/clusters/nasa-cryo/common.values.yaml @@ -119,7 +119,7 @@ basehub: description: &profile_list_description "Start a container with at least a chosen share of capacity on a node of this type" slug: small default: true - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - CryoInTheCloud:cryoclouduser - CryoInTheCloud:cryocloudadvanced @@ -200,7 +200,7 @@ basehub: - display_name: "Medium: up to 16 CPU / 128 GB RAM" description: *profile_list_description slug: medium - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - CryoInTheCloud:cryocloudadvanced - CryoInTheCloud:ml-in-glaciology @@ -268,7 +268,7 @@ basehub: # - display_name: "Large: up to 64 CPU / 512 GB RAM" # description: *profile_list_description # slug: large - # allowed_teams: + # allowed_groups: # - 2i2c-org:hub-access-for-2i2c-staff # - CryoInTheCloud:cryocloudadvanced # profile_options: @@ -328,7 +328,7 @@ basehub: - display_name: NVIDIA Tesla T4, ~16 GB, ~4 CPUs description: "Start a container on a dedicated node with a GPU" slug: "gpu" - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - CryoInTheCloud:ml-in-glaciology profile_options: diff --git a/config/clusters/openscapes/common.values.yaml b/config/clusters/openscapes/common.values.yaml index 138242ec7..ed41fb977 100644 --- a/config/clusters/openscapes/common.values.yaml +++ b/config/clusters/openscapes/common.values.yaml @@ -45,7 +45,7 @@ basehub: - display_name: Python description: Python datascience environment default: true - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - NASA-Openscapes:workshopaccess-2i2c # legacy but no plans to delete immediately until fledged - NASA-Openscapes:longtermaccess-2i2c @@ -128,7 +128,7 @@ basehub: node.kubernetes.io/instance-type: r5.4xlarge - display_name: R description: R (with RStudio) + Python environment - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - NASA-Openscapes:workshopaccess-2i2c - NASA-Openscapes:longtermaccess-2i2c @@ -147,7 +147,7 @@ basehub: profile_options: *profile_options - display_name: Matlab description: Matlab environment - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - NASA-Openscapes:workshopaccess-2i2c - NASA-Openscapes:longtermaccess-2i2c @@ -164,7 +164,7 @@ basehub: - display_name: "Bring your own image" description: Specify your own docker image (must have python and jupyterhub installed in it) slug: custom - allowed_teams: + allowed_groups: - NASA-Openscapes:longtermaccess-2i2c - 2i2c-org:hub-access-for-2i2c-staff # Requested in: https://2i2c.freshdesk.com/a/tickets/1284 diff --git a/config/clusters/pangeo-hubs/common.values.yaml b/config/clusters/pangeo-hubs/common.values.yaml index 96543bb82..3bb38ef5c 100644 --- a/config/clusters/pangeo-hubs/common.values.yaml +++ b/config/clusters/pangeo-hubs/common.values.yaml @@ -62,7 +62,7 @@ basehub: - display_name: "Small" description: 5GB RAM, 2 CPUs default: true - allowed_teams: + allowed_groups: - pangeo-data:us-central1-b-gcp - 2i2c-org:hub-access-for-2i2c-staff kubespawner_override: @@ -72,7 +72,7 @@ basehub: node.kubernetes.io/instance-type: n1-standard-2 - display_name: Medium description: 11GB RAM, 4 CPUs - allowed_teams: + allowed_groups: - pangeo-data:us-central1-b-gcp - 2i2c-org:hub-access-for-2i2c-staff kubespawner_override: @@ -82,7 +82,7 @@ basehub: node.kubernetes.io/instance-type: n1-standard-4 - display_name: Large description: 24GB RAM, 8 CPUs - allowed_teams: + allowed_groups: - pangeo-data:cds-lab - 2i2c-org:hub-access-for-2i2c-staff kubespawner_override: @@ -92,7 +92,7 @@ basehub: node.kubernetes.io/instance-type: n1-standard-8 - display_name: Huge description: 52GB RAM, 16 CPUs - allowed_teams: + allowed_groups: - pangeo-data:cds-lab - 2i2c-org:hub-access-for-2i2c-staff kubespawner_override: diff --git a/config/clusters/smithsonian/common.values.yaml b/config/clusters/smithsonian/common.values.yaml index a33202123..b6ee63229 100644 --- a/config/clusters/smithsonian/common.values.yaml +++ b/config/clusters/smithsonian/common.values.yaml @@ -168,7 +168,7 @@ basehub: - display_name: NVIDIA Tesla T4, ~16 GB, ~4 CPUs slug: gpu description: "Start a container on a dedicated node with a GPU" - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - Smithsonian-SDCH:gpu-users profile_options: diff --git a/docs/howto/features/allow-unlisted-profile-choice.md b/docs/howto/features/allow-unlisted-profile-choice.md index 85b48d60a..7414c2505 100644 --- a/docs/howto/features/allow-unlisted-profile-choice.md +++ b/docs/howto/features/allow-unlisted-profile-choice.md @@ -1,3 +1,4 @@ +(howto:features:unlisted-choice)= # Allow users to setup custom, free-form user profile choices Sometimes it is useful to allow users to specify their own, free-form choice for an option. @@ -38,7 +39,7 @@ jupyterhub: In some hubs, we don't want *everyone* to be able to specify an image - but we do want some subset of users to be able to do so, for testing purposes. This can be done by coupling `unlisted_choice` with -[`allowed_teams`](auth:github-orgs:profile-list). +[`allowed_groups`](howto:features:profile-restrict). In the `profileList` for the hub in question, add a profile like this: @@ -46,7 +47,7 @@ In the `profileList` for the hub in question, add a profile like this: - display_name: "Test custom image" description: Test any custom image before rolling it out to rest of your users slug: custom-image-only - allowed_teams: + allowed_groups: - 2i2c-org:hub-access-for-2i2c-staff - profile_options: diff --git a/docs/howto/features/gpu.md b/docs/howto/features/gpu.md index 5b059a2b7..2f1b78dfc 100644 --- a/docs/howto/features/gpu.md +++ b/docs/howto/features/gpu.md @@ -214,7 +214,7 @@ jupyterhub: using `GitHubOAuthenticator`, and restricts access to the GPU only to members of that GitHub team. If `allowed_teams` is not used in other config in the `profileList`, you may need to also explicitly - [enable some other config (`enable_auth_state` and `populate_teams_in_auth_state`)](auth:github-orgs:profile-list) + [enable some other config (`enable_auth_state` and `populate_teams_in_auth_state`)](howto:features:profile-restrict) for this feature to work. Do a deployment with this config, and then we can test to make sure diff --git a/docs/howto/features/index.md b/docs/howto/features/index.md index 84d44851a..a50f94a80 100644 --- a/docs/howto/features/index.md +++ b/docs/howto/features/index.md @@ -8,6 +8,7 @@ See the sections below for more details. ```{toctree} :maxdepth: 2 allow-unlisted-profile-choice.md +profile-list-restrict.md anonymized-usernames.md buckets.md cloud-access.md diff --git a/docs/howto/features/profile-list-restrict.md b/docs/howto/features/profile-list-restrict.md new file mode 100644 index 000000000..ba021e89e --- /dev/null +++ b/docs/howto/features/profile-list-restrict.md @@ -0,0 +1,212 @@ +(howto:features:profile-restrict)= +# Restrict profile options based on JupyterHub groups (or GitHub teams) + +```{warning} +This is currently only functional for GitHub authentication with `GitHubOAuthenticator`, +and the earthscope hub is special cased. This will be more generally available once +group management is [broadly available](https://github.com/jupyterhub/oauthenticator/pull/735) +on OAuthenticator. +``` + +Communities often want to *selectively* grant access to resources based on +what *groups* a user belongs to. The most common example being restricted +access to GPUs, really large resource allocations or the ability to specify +[arbitrary images to launch](howto:features:unlisted-choice). + +We override the [`profile_list`](https://jupyterhub-kubespawner.readthedocs.io/en/latest/spawner.html#kubespawner.KubeSpawner.profile_list) +feature of KubeSpawner to be able to restrict specific profiles or profile options +to only be available to users who belong to specific JupyterHub groups (or in the +case of using GitHub authentication, GitHub teams). + +## The `allowed_groups` configuration + +The key `allowed_groups` can be set under: + +1. Any **Profile** - the first level of options shown in the profile selection screen + to the user, selectable via radio buttons. +2. Any **Profile Option Choice** - the second level of options shown in the profile + selection screen to the user, selectable via dropdown box. +3. Any **Profile Option Unlisted Choice** - the optional way for end users to write in + an arbitrary value to be used, often for selecting the image to be run, instead of + selecting one of the pre-determined values. + +It can contain a list of JupyterHub **group names**. These are determined either via +the JupyterHub admin interface or provided externally via the authentication provider +(see below for how the group names look like) + +If `allowed_groups` is not set, that profile, profile option choice or unlisted choice, will be visible and available to everyone who can log in to the hub/ + +So to restrict a profile, profile option choice or unlisted choice to a specific set +of users, put a `allowed_groups` config under whatever you want to restrict, and list +the groups that should be *allowed* access. Everyone else will not see that option, +and members of that group will. + +Now let's look at some examples. + +### Example 1: Restrict an entire profile + +Let's say a community wants to restrict a Matlab profile only to a select few +users, but their python environment be available to everyone. And they are using GitHub as their +authentication provider, with `GitHubOAuthenticator`. + +We would have a `profileList` like this: + +```yaml +- display_name: Python + description: Python datascience environment + default: true + kubespawner_override: + image: python-image:tag + profile_options: &profile_options + requests: &profile_options_resource_allocation + display_name: Resource Allocation + choices: + mem_1_9: + display_name: 1.9 GB RAM, upto 3.7 CPUs + kubespawner_override: + mem_guarantee: 1991244775 + mem_limit: 1991244775 + cpu_guarantee: 0.2328125 + cpu_limit: 3.725 + node_selector: + node.kubernetes.io/instance-type: r5.xlarge + default: true + mem_3_7: + display_name: 3.7 GB RAM, upto 3.7 CPUs + kubespawner_override: + mem_guarantee: 3982489550 + mem_limit: 3982489550 + cpu_guarantee: 0.465625 + cpu_limit: 3.725 + node_selector: + node.kubernetes.io/instance-type: r5.xlarge + mem_7_4: + display_name: 7.4 GB RAM, upto 3.7 CPUs + kubespawner_override: + mem_guarantee: 7964979101 + mem_limit: 7964979101 + cpu_guarantee: 0.93125 + cpu_limit: 3.725 + node_selector: + node.kubernetes.io/instance-type: r5.xlarge +- display_name: Matlab + description: Matlab environment + allowed_groups: + - 2i2c-org:hub-access-for-2i2c-staff + - organization:matlab-access + kubespawner_override: + image: matlab-image:tag + profile_options: *profile_options +``` + +The `Python` profile does not have an `allowed_groups` set, so everyone who can +log in to the hub can use that. The `Matlab` profile has an `allowed_groups` set, +and allows two groups - one specifically for 2i2c staff members, and another for +those the community has added to a `matlab-access` team inside their GitHub org. + +### Example 2: Restrict a particular `profile_option` choice + +Now let's say the community wants to restrict only users who are members of a +`large-compute` team to access the `7.4 GB RAM, upto 3.7 CPUs` and +`3.7 GB RAM, upto 3.7 CPUs` profile option. + +```yaml +- display_name: Python + description: Python datascience environment + default: true + kubespawner_override: + image: python-image:tag + profile_options: &profile_options + requests: &profile_options_resource_allocation + display_name: Resource Allocation + choices: + mem_1_9: + display_name: 1.9 GB RAM, upto 3.7 CPUs + kubespawner_override: + mem_guarantee: 1991244775 + mem_limit: 1991244775 + cpu_guarantee: 0.2328125 + cpu_limit: 3.725 + node_selector: + node.kubernetes.io/instance-type: r5.xlarge + default: true + mem_3_7: + display_name: 3.7 GB RAM, upto 3.7 CPUs + allowed_groups: + - 2i2c-org:hub-access-for-2i2c-staff + - organization:large-compute + kubespawner_override: + mem_guarantee: 3982489550 + mem_limit: 3982489550 + cpu_guarantee: 0.465625 + cpu_limit: 3.725 + node_selector: + node.kubernetes.io/instance-type: r5.xlarge + mem_7_4: + display_name: 7.4 GB RAM, upto 3.7 CPUs + allowed_groups: + - 2i2c-org:hub-access-for-2i2c-staff + - organization:large-compute + kubespawner_override: + mem_guarantee: 7964979101 + mem_limit: 7964979101 + cpu_guarantee: 0.93125 + cpu_limit: 3.725 + node_selector: + node.kubernetes.io/instance-type: r5.xlarge +- display_name: Matlab + description: Matlab environment + allowed_groups: + - 2i2c-org:hub-access-for-2i2c-staff + - organization:matlab-access + kubespawner_override: + image: matlab-image:tag + profile_options: *profile_options +``` + +Since this adds on from the previous example, it'll have the following behavior: + +1. Everyone can see and use the Python profile +2. Only members of the team `matlab-access` (and 2i2c staff) can see the `Matlab` + profile. +3. Only members of the team `large-compute` can see the two larger dropdown + items. This `profile_option` is the same for both python and matlab, so this + behavior is repeated for both of them. +4. So only people who are members of *both* `matlab-access` and `large-compute` can + see the larger dropdown options for the Matlab profile. + +## Enabling externally managed groups for `GitHubOAuthenticator` + +For hubs using `GitHubOAuthenticator`, groups (for the purposes of this feature alone) +are of the form `:`. + +The following extra config is also required to enable this feature. + +```yaml +jupyterhub: + hub: + config: + Authenticator: + enable_auth_state: true + GitHubOAuthenticator: + populate_teams_in_auth_state: true +``` + +```{note} +GitHubOAuthenticator is currently special cased in our code, until +[this PR](https://github.com/jupyterhub/oauthenticator/pull/735) is merged +and deployed. `allowed_groups` will treat GitHub team membership as groups, +but other JupyterHub functionality that depends on groups will not. +``` + +### Enabling access for 2i2c engineers + +All 2i2c engineers are part of the GitHub team `2i2c-org:hub-access-for-2i2c-staff`, so +every `allowed_group` entry should have an explicit mention of that team so 2i2c engineers +can access that option / profile and test it out when needed. + +## Enabling this feature for other Authenticators + +Currently, the EarthScope hub has this feature enabled via custom overrides. Once +[this PR](https://github.com/jupyterhub/oauthenticator/pull/735) is merged and +deployed, we can enable this feature for hubs using other Authenticators more generally. \ No newline at end of file diff --git a/docs/hub-deployment-guide/configure-auth/auth0.md b/docs/hub-deployment-guide/configure-auth/auth0.md index ff5f34d6a..80a2d222d 100644 --- a/docs/hub-deployment-guide/configure-auth/auth0.md +++ b/docs/hub-deployment-guide/configure-auth/auth0.md @@ -43,17 +43,20 @@ administer. Solutions (potentially a shared account) are being explored. ## Configuring the JupyterHub to use Auth0 -We will use the upstream [Auth0OAuthenticator](https://github.com/jupyterhub/oauthenticator/blob/main/oauthenticator/auth0.py) -to allow folks to login to JupyterHub. +While there is an upstream [Auth0OAuthenticator](https://github.com/jupyterhub/oauthenticator/blob/main/oauthenticator/auth0.py), +it doesn't have any specific features that aren't in the upstream [GenericOAuthenticator](https://oauthenticator.readthedocs.io/en/latest/reference/api/gen/oauthenticator.generic.html), +and is missing some features that are present in the GenericOAuthenticator. Using the GenericOAuthenticator +here also allows us to support other Generic OAuth providers in the future, and not tie ourselves down +to Auth0. -In the `common.yaml` file for the cluster hosting the hubs, we set the authenticator to be `auth0`. +In the `common.yaml` file for the cluster hosting the hubs, we set the authenticator to be `generic`. ```yaml jupyterhub: hub: config: JupyterHub: - authenticator_class: auth0 + authenticator_class: generic ``` In the encrypted, per-hub config (of form `enc-.secret.values.yaml`), we specify the secret values @@ -63,9 +66,10 @@ we received from the community. jupyterhub: hub: config: - Auth0OAuthenticator: + GenericOAuthenticator: client_id: client_secret: + logout_redirect_url: https:///v2/logout?client_id= ``` And in the *unencrypted*, per-hub config (of form `.values.yaml`), we specify the non-secret @@ -75,12 +79,18 @@ config values. jupyterhub: hub: config: - Auth0OAuthenticator: - auth0_domain: + GenericOAuthenticator: + token_url: https:///oauth/token + authorize_url: https:///authorize + userdata_url: https:///userinfo scope: openid username_claim: sub ``` +Auth0 has documentation for the [userinfo](https://auth0.com/docs/api/authentication#get-user-info), +[token](https://auth0.com/docs/api/authentication#authenticate-user) and [authorize](https://auth0.com/docs/api/authentication#social) +endpoints. + Once deployed, this should allow users authorized by Auth0 to login to the hub! Their usernames will look like `:`, which looks a little strange but allows differentiation between people who use multiple accounts but the same email. \ No newline at end of file diff --git a/docs/hub-deployment-guide/configure-auth/github-orgs.md b/docs/hub-deployment-guide/configure-auth/github-orgs.md index e02161950..4bee84530 100644 --- a/docs/hub-deployment-guide/configure-auth/github-orgs.md +++ b/docs/hub-deployment-guide/configure-auth/github-orgs.md @@ -132,7 +132,6 @@ You will **still** require admin access to the org to carry out those steps. Once you have confirmed with the Community Representative that users can login, you can remove yourself from the org. -(auth:github-orgs:profile-list)= ## Restricting user profiles based on GitHub Team Membership JupyterHub has support for using [profileList](https://zero-to-jupyterhub.readthedocs.io/en/latest/jupyterhub/customizing/user-environment.html#using-multiple-profiles-to-let-users-select-their-environment) @@ -141,72 +140,7 @@ server. In addition, we can allow people access to specific profiles based on their GitHub Teams membership! This only works if the hub is already set to allow people only from certain GitHub organizations -to log in. - -The key `allowed_teams` can be set for any profile definition, with a list of GitHub -teams (formatted as `:`) that will get access to that profile. Users -need to be a member of any one of the listed teams for access. The list of teams a user -is part of is fetched at login time - so if the user is added to a GitHub team, they need -to log out and log back in to the JupyterHub (not necessarily to GitHub!) to see the new -profiles they have access to. To remove access to a profile from a user, they have to be -removed from the appropriate team on GitHub *and* their JupyterHub user needs to be -deleted from the hub admin dashboard. - -To enable this access, - -1. Enable storing the list of GitHub teams a user is in as a part of - [`auth_state`](https://zero-to-jupyterhub.readthedocs.io/en/latest/administrator/authentication.html#enable-auth-state) - with the following config: - - ```yaml - jupyterhub: - hub: - config: - Authenticator: - enable_auth_state: true - GitHubOAuthenticator: - populate_teams_in_auth_state: true - ``` - - If `populate_teams_in_auth_state` is not set, this entire feature is disabled. - -2. Specify which teams should have access to which profiles with an - `allowed_teams` key under `profileList`: - - ```yaml - jupyterhub: - singleuser: - profileList: - - display_name: Small - description: 1.0 GB RAM - default: true - allowed_teams: - - : - - 2i2c-org:hub-access-for-2i2c-staff - kubespawner_override: - mem_guarantee: 1G - mem_limit: 1G - - display_name: Medium - description: 4.0 GB RAM - allowed_teams: - - : - - 2i2c-org:hub-access-for-2i2c-staff - kubespawner_override: - mem_guarantee: 4G - mem_limit: 4G - ``` - - Users who are a part of *any* of the listed teams will be able to access - that profile. Add `2i2c-org:hub-access-for-2i2c-staff` to all - `allowed_teams` so 2i2c engineers can log in to debug issues. If - `allowed_teams` is not set, that profile is not available to anyone. - - ```{note} - We used to allow restricting which profiles users can see based on what - org they were a part of, rather than just the *teams* they were a part of. - We no longer support this. - ``` - +to log in. See [](howto:features:profile-restrict) for more information. ### Enabling team based access on hub with pre-existing users If this is being enabled for users on a hub with *pre-existing* users, they diff --git a/helm-charts/basehub/values.yaml b/helm-charts/basehub/values.yaml index f7c7f0fb5..efa847694 100644 --- a/helm-charts/basehub/values.yaml +++ b/helm-charts/basehub/values.yaml @@ -396,7 +396,7 @@ jupyterhub: ) if proc.returncode == 0: return True - return False + return False def main(): @@ -987,10 +987,10 @@ jupyterhub: timeout=spawner.k8s_api_request_retry_timeout ) c.Spawner.pre_spawn_hook = ensure_db_pvc - 05-gh-teams: | + 05-profile-groups: | # Re-assignes c.KubeSpawner.profile_list to a callable that filters the # initial configuration of profile_list based on the user's github - # org/team membership as declared via "allowed_teams" read from + # org/team membership as declared via "allowed_groups" read from # profile_list profiles. # # This only has effect if: @@ -1000,64 +1000,92 @@ jupyterhub: # requires Authenticator.enable_auth_state to be True as well. # - The user is a normal user, and not "deployment-service-check". # - import copy + from copy import deepcopy + from functools import partial from textwrap import dedent from tornado import web from oauthenticator.github import GitHubOAuthenticator + from z2jh import get_config - original_profile_list = c.KubeSpawner.profile_list - - async def profile_list_allowed_teams_filter(spawner): + async def profile_list_allowed_groups_filter(original_profile_list, spawner): """ Returns the initially configured profile_list filtered based on the - user's membership in each profile's `allowed_teams`. If - `allowed_teams` isn't set for a profile, its not filtered out. + user's membership in each profile's `allowed_groups`. If + `allowed_groups` isn't set for a profile, that profile is allowed for + everyone. Similar functionality is provided for both `unlisted_choice` and + `choice` inside `profile_options`. - `allowed_teams` is a list of GitHub organizations and/or teams - specified with `` or `:` strings. + `allowed_groups` is a list of JupyterHub groups, set up by the authenticator. + In addition, for use with GitHubOAuthenticator, it can be a list of + teams the user is a part of, of form ':'. - If the returned profile_list is filtered to not include a profile, + If the returned profile_list is filtered to not include any profiles, an error is raised and the user isn't allowed to start a server. """ - # Ensure GitHubOAuthenticator with populate_teams_in_auth_state set - if not isinstance(spawner.authenticator, GitHubOAuthenticator): - return original_profile_list - if not spawner.authenticator.populate_teams_in_auth_state: - return original_profile_list if spawner.user.name == "deployment-service-check": - print("Ignoring allowed_teams check for deployment-service-check") + print("Ignoring allowed_groups check for deployment-service-check") return original_profile_list - # Ensure auth_state is populated with teams info - auth_state = await spawner.user.get_auth_state() - if not auth_state or "teams" not in auth_state: - print(f"User {spawner.user.name} does not have any auth_state set") - raise web.HTTPError(403) + # casefold group names so we can do case insensitive comparisons. + groups = {g.name.casefold() for g in spawner.user.groups} - # Format user's teams in auth_state to "org:team" - # casefold them so we can do case insensitive comparisons, as github itself is case insensitive (but preserving) - # for orgs and teams - teams = set([f'{team["organization"]["login"]}:{team["slug"]}'.casefold() for team in auth_state["teams"]]) - print(f"User {spawner.user.name} is part of teams {' '.join(teams)}") + # If we're using GitHubOAuthenticator, add the user's teams to the groups as well. + # Eventually this can be removed, as the user's teams can be set to be groups + # once https://github.com/jupyterhub/oauthenticator/pull/735 is merged + if isinstance(spawner.authenticator, GitHubOAuthenticator): + # Ensure auth_state is populated with teams info + auth_state = await spawner.user.get_auth_state() + if not auth_state or "teams" not in auth_state: + print(f"User {spawner.user.name} does not have any auth_state set, profile_list filtering not available") - # Filter out profiles with allowed_teams set if the user isn't part - # of any. - allowed_profiles = [] - for profile in copy.deepcopy(original_profile_list): - allowed_teams = profile.get("allowed_teams") - if allowed_teams is None: - allowed_profiles.append(profile) - continue + else: + # casefold teams to match what GitHub's API does when doing authorization calls + groups |= set([f'{team["organization"]["login"]}:{team["slug"]}'.casefold() for team in auth_state["teams"]]) - # casefold teams so we can do case insensitive comparisons, as github itself is case insensitive (but preserving) - # for orgs and teams - allowed_teams = set([t.casefold() for t in allowed_teams if ':' in t]) + print(f"User {spawner.user.name} is part of groups {' '.join(groups)}") - if allowed_teams & teams: - print(f"Allowing profile {profile['display_name']} for user {spawner.user.name} based on team membership") - allowed_profiles.append(profile) - continue + # Filter out profiles with allowed_groups set if the user isn't part of the group + allowed_profiles = [] + for original_profile in original_profile_list: + # Make a copy, as we'll be modifying this profile + profile = deepcopy(original_profile) + + # Handle `allowed_groups` specified in profile_options + if 'profile_options' in profile: + for k, po in profile['profile_options'].items(): + + # If `unlisted_choice` has an `allowed_groups` and the current + # user is not present in any of those teams, we delete the + # `unlisted_choice` config entirely for this option. The user + # will then not be allowed to 'write in' a value. + if 'unlisted_choice' in po: + if 'allowed_groups' in po['unlisted_choice']: + if not (set(po['unlisted_choice']['allowed_groups']) and groups): + del po['unlisted_choice'] + + if 'choices' in po: + new_choices = {} + for k, c in po['choices'].items(): + # If `allowed_groups` is not set for a profile option, it is automatically + # allowed for everyone + if 'allowed_groups' not in c: + new_choices[k] = c + # If `allowed_groups` *is* set for a profile option, it is allowed only for + # members of that team. + elif set(c['allowed_groups']) & groups: + new_choices[k] = c + po['choices'] = new_choices + + if 'allowed_groups' not in profile: + allowed_profiles.append(profile) + else: + allowed_groups = set([g.casefold() for g in profile.get("allowed_groups", [])]) + + if allowed_groups & groups: + print(f"Allowing profile {profile['display_name']} for user {spawner.user.name} based on team membership") + allowed_profiles.append(profile) + continue if len(allowed_profiles) == 0: # If no profiles are allowed, user should not be able to spawn anything! @@ -1065,23 +1093,27 @@ jupyterhub: # set in singleuser, without any profile overrides. Not desired behavior # FIXME: User doesn't actually see this error message, just the generic 403. error_msg = dedent(f""" - Your GitHub team membership is insufficient to launch any server profiles. + Your JupyterHub group membership is insufficient to launch any server profiles. - GitHub teams you are a member of that this JupyterHub knows about are {', '.join(teams)}. + JupyterHub groups you are a member of are {', '.join(groups)}. - If you are part of additional teams, log out of this JupyterHub and log back in to refresh that information. + If you are part of additional groups, log out of this JupyterHub and log back in to refresh that information. """) raise web.HTTPError(403, error_msg) return allowed_profiles - # Only set this customized profile_list *if* we already have a profile_list set - # otherwise, we'll show users a blank server options form and they won't be able to - # start their server + # Only set our custom filter if + # profile_list is specified (otherwise users will get an empty screen when trying to launch servers) if c.KubeSpawner.profile_list: # Customize list of profiles dynamically, rather than override options form. # This is more secure, as users can't override the options available to them via the hub API - c.KubeSpawner.profile_list = profile_list_allowed_teams_filter + # We pass in a copy of the original profile_list set in config via partial, to reduce possible variable + # capture related issues. + c.KubeSpawner.profile_list = partial( + profile_list_allowed_groups_filter, + deepcopy(c.KubeSpawner.profile_list) + ) 06-salted-username: | # Allow anonymizing username to not store *any* PII