From f2e46a38ea2f40b3b3478e9d456333d73db2e5c5 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 2 Mar 2022 12:55:10 +0200 Subject: [PATCH 01/70] Temporarly use CILogon JupyterHub OAuthenticator for the staging hub --- config/clusters/2i2c/cluster.yaml | 3 ++- config/clusters/2i2c/staging.values.yaml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index 822578cdf5..b1c698936a 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -18,7 +18,8 @@ hubs: auth0: # connection update? Also ensure the basehub Helm chart is provided a # matching value for jupyterhub.custom.2i2c.add_staff_user_ids_of_type! - connection: google-oauth2 + # connection: google-oauth2 + enabled: false helm_chart_values_files: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check diff --git a/config/clusters/2i2c/staging.values.yaml b/config/clusters/2i2c/staging.values.yaml index 8cd8eced20..3058b2f4e4 100644 --- a/config/clusters/2i2c/staging.values.yaml +++ b/config/clusters/2i2c/staging.values.yaml @@ -28,3 +28,7 @@ jupyterhub: allowed_users: &staging_users - colliand@gmail.com admin_users: *staging_users + JupyterHub: + authenticator_class: cilogon + CILogonOAuthenticator: + username_claim: "email" From 6540d37e6423115ea042ed11e075f878606c68a1 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 3 Mar 2022 15:16:42 +0200 Subject: [PATCH 02/70] First version of a cilogon client provider --- deployer/cilogon_auth.py | 149 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 deployer/cilogon_auth.py diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py new file mode 100644 index 0000000000..f960bfe321 --- /dev/null +++ b/deployer/cilogon_auth.py @@ -0,0 +1,149 @@ +from yarl import URL + +import base64 +import requests + +class CILogonAdmin: + timeout = 5.0 + + def __init__(self, admin_id, admin_secret): + self.admin_id = admin_id + self.admin_secret = admin_secret + + bearer_token = base64.urlsafe_b64encode(f"{self.admin_id}:{self.admin_secret}") + + self.base_headers = { + 'Authorization': f'Bearer {bearer_token}', + 'Content-Type': 'application/json', + } + + + def _url(self, id=None): + url = "https://cilogon.org/oauth2/oidc-cm" + + if id is None: + return url + + return str(URL(url).with_query({"client_id": id})) + + + def _post(self, url, data=None): + headers = self.base_headers.copy() + + return requests.post(url, json=data, headers=headers, timeout=self.timeout) + + def _get(self, url, params=None): + # todo: make this resilient, make use of exponentiall_backoff + headers = self.base_headers.copy() + + return requests.get(url, params=params, headers=headers, timeout=self.timeout) + + def _put(self, url, data=None): + headers = self.base_headers.copy() + + return requests.put(url, json=data, headers=headers, timeout=self.timeout) + + def create(self, body): + """Creates a new client + + Args: + body (dict): Attributes for the new client + + See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-post.sh + """ + + return self._post(self._url(), data=body) + + def get(self, id): + """Retrieves a client by its id. + + Args: + id (str): Id of the client to get + + See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-get.sh + """ + + return self.client.get(self._url(id)) + + def update(self, id, body): + """Modifies a client by its id. + + The client_secret attribute cannot be updated. + Note that any values missing will be deleted from the information for the server! + Args: + id (str): Id of the client to modify + body (dict): Attributes to modify. + + See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-put.sh + """ + + return self.client.put(self._url(id), data=body) + + +class ClientProvider: + def __init__(self, admin_id, admin_secret): + self.admin_id = admin_id + self.admin_secret = admin_secret + + @property + def cilogon(self): + """ + Return a CILogonAdmin instance + """ + if not hasattr(self, "_cilogon_admin"): + self._cilogon_admin = CILogonAdmin(self.admin_id, self.admin_secret) + + return self._cilogon_admin + + def create_client(self, name, callback_url): + client_details = { + "client_name": name, + "app_type": "web", + "redirect_uris": [callback_url], + "scope": "openid email org.cilogon.userinfo", + } + + created_client = self.cilogon.create(client_details) + + return created_client + + def update_client(self, name, callback_url): + client_details = { + "client_name": name, + "app_type": "web", + "redirect_uris": [callback_url], + "scope": "openid email org.cilogon.userinfo", + } + + created_client = self.cilogon.update(client_details) + + return created_client + + + def ensure_client(self, name, callback_url): + client = self.get(name) + + if client is None or client["registration_client_uri"] is None: + # Create the client, all good + client = self.create_client(name, callback_url) + else: + # Update the client + client = self.update_client(name, callback_url) + + return client + + def get_client_creds(self, client, connection_name): + """ + Return z2jh config for cilogon authentication for this JupyterHub + """ + + auth = { + "client_id": client["client_id"], + "client_secret": client["client_secret"], + "username_claim": "nickname" if connection_name == "github" else "email", + "scope": client["scope"], + "logout_redirect_url": "https://cilogon.org/logout/", + "allowed_idps": [connection_name] + } + + return auth From 6bddb3b3a5d5bb8de4cd87643e009b618098b2d6 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Fri, 11 Mar 2022 23:49:54 +0200 Subject: [PATCH 03/70] Add schema defs for cilogon --- shared/deployer/cluster.schema.yaml | 33 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/shared/deployer/cluster.schema.yaml b/shared/deployer/cluster.schema.yaml index 5e91dee904..b1a1dd866e 100644 --- a/shared/deployer/cluster.schema.yaml +++ b/shared/deployer/cluster.schema.yaml @@ -241,14 +241,6 @@ properties: Authentication method users of the hub can use to log in to the hub. We support a subset of the [connectors](https://auth0.com/docs/identityproviders) that auth0 supports - application_name: - type: string - description: | - We make use of an OAuth2 applications in Auth0 and this is the - name of that application. Defaults to - "-". - - See https://manage.auth0.com/dashboard/us/2i2c/applications. password: type: object description: | @@ -270,6 +262,31 @@ properties: then: required: - connection + cilogon: + additionalProperties: false + type: object + description: | + Some hubs use CILogon for authentication, and we dynamically fetch the credentials + needed for each hub - client_id, client_secret, callback_url - on deploy. This + block contains configuration on how cilogon should be configured for this hub. + properties: + enabled: + type: boolean + default: false + description: | + Whether or not to enable CILogon authentication for this hub. + Defaults to false + allowed_idps: + type: string + description: | + Allowed identity providers. + if: + properties: + enabled: + const: true + then: + required: + - allowed_idps helm_chart_values_files: type: array description: | From ebdee91705364f459418e20c91951f023bc08cc1 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Sat, 12 Mar 2022 01:23:56 +0200 Subject: [PATCH 04/70] Enable new CIlogon for dask-staging --- config/clusters/2i2c/cluster.yaml | 9 ++++++--- config/clusters/2i2c/staging.values.yaml | 4 ---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index b1c698936a..ea90ee86e8 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -18,8 +18,7 @@ hubs: auth0: # connection update? Also ensure the basehub Helm chart is provided a # matching value for jupyterhub.custom.2i2c.add_staff_user_ids_of_type! - # connection: google-oauth2 - enabled: false + connection: google-oauth2 helm_chart_values_files: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check @@ -32,7 +31,11 @@ hubs: auth0: # connection update? Also ensure the basehub Helm chart is provided a # matching value for jupyterhub.custom.2i2c.add_staff_user_ids_of_type! - connection: google-oauth2 + enabled: false + # connection: google-oauth2 + cilogon: + enabled: true + allowed_idps: "2i2c.org" helm_chart_values_files: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check diff --git a/config/clusters/2i2c/staging.values.yaml b/config/clusters/2i2c/staging.values.yaml index 3058b2f4e4..8cd8eced20 100644 --- a/config/clusters/2i2c/staging.values.yaml +++ b/config/clusters/2i2c/staging.values.yaml @@ -28,7 +28,3 @@ jupyterhub: allowed_users: &staging_users - colliand@gmail.com admin_users: *staging_users - JupyterHub: - authenticator_class: cilogon - CILogonOAuthenticator: - username_claim: "email" From d6df2a901c37bc9902a84bc786a4326d16019eab Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Sat, 12 Mar 2022 01:24:57 +0200 Subject: [PATCH 05/70] Add interface class --- deployer/auth_client_provider.py | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 deployer/auth_client_provider.py diff --git a/deployer/auth_client_provider.py b/deployer/auth_client_provider.py new file mode 100644 index 0000000000..69be78ee28 --- /dev/null +++ b/deployer/auth_client_provider.py @@ -0,0 +1,53 @@ +class ClientProvider: + @property + def admin_client(self): + """Return an administrative instance of the authentication client. + + This instance must be able to create/update/delete other clients. + """ + + pass + + def create_client(self, name, callback_url, logout_url=None): + """Create a OAuth2 client application. + + **Subclasses must define this method** + + Args: + name (str): human readable name of the client + callback_url (str): URL that is invoked after OAuth authorization + (optional) logout_url (string): URL to redirect users to after app logout + """ + pass + + def ensure_client( + self, name, callback_url, logout_url=None, connection_name=None, connection_config=None + ): + """Ensures a client with the specified name and config exists. + If one doesn't exist, create it and if it does exist, update it + with the specified config. + + **Subclasses must define this method** + + Args: + name (str): human readable name of the client + callback_url (str): URL that is invoked after OAuth authorization + (optional) logout_url (string): URL to redirect users to after app logout + (optional) connection_name: The name of the connection/identity provider + (optional) connection_config: Extra configuration to be passed to the chosen connection + + Should return a dict describing the client created/updated. + + """ + pass + + def get_client_creds(self, client, connection_name=None, callback_url=None): + """Return z2jh config for auth0 authentication for this JupyterHub. + + Args: + client (dict): OAuth2 clientvv - must include `client_id` and `client_secret` keys + (optional) callback_url (str): URL that is invoked after OAuth authorization + (optional) connection_name: The name of the connection/identity provider + """ + + pass \ No newline at end of file From 8d17070658de255644571c971c61bfc5bcff2d5e Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Sat, 12 Mar 2022 01:26:19 +0200 Subject: [PATCH 06/70] Update Auth0 class to follow base class --- deployer/auth.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/deployer/auth.py b/deployer/auth.py index 117d6b6860..b6c38c6379 100644 --- a/deployer/auth.py +++ b/deployer/auth.py @@ -4,6 +4,8 @@ from auth0.v3.management import Auth0 from yarl import URL +from auth_client_provider import ClientProvider + # What key in the authenticated user's profile to use as hub username # This shouldn't be changeable by the user! USERNAME_KEYS = { @@ -14,36 +16,36 @@ } -class KeyProvider: - def __init__(self, domain, client_id, client_secret): +class Auth0ClientProvider(ClientProvider): + def __init__(self, client_id, client_secret, domain): self.client_id = client_id self.client_secret = client_secret self.domain = domain @property - def auth0(self): + def admin_client(self): """ Return an authenticated Auth0 instance """ - if not hasattr(self, "_auth0"): + if not hasattr(self, "_admin_client"): gt = GetToken(self.domain) creds = gt.client_credentials( self.client_id, self.client_secret, f"https://{self.domain}/api/v2/" ) - self._auth0 = Auth0(self.domain, creds["access_token"]) - return self._auth0 + self._admin_client = Auth0(self.domain, creds["access_token"]) + return self._admin_client - def get_clients(self): + def _get_clients(self): return { client["name"]: client # Our account is limited to 100 clients, and we want it all in one go - for client in self.auth0.clients.all(per_page=100) + for client in self.admin_client.clients.all(per_page=100) } - def get_connections(self): + def _get_connections(self): return { connection["name"]: connection - for connection in self.auth0.connections.all() + for connection in self.admin_client.connections.all() } def create_client(self, name, callback_url, logout_url): @@ -53,7 +55,7 @@ def create_client(self, name, callback_url, logout_url): "callbacks": [callback_url], "allowed_logout_urls": [logout_url], } - created_client = self.auth0.clients.create(client) + created_client = self.admin_client.clients.create(client) return created_client def _ensure_client_callback(self, client, callback_url): @@ -61,7 +63,7 @@ def _ensure_client_callback(self, client, callback_url): Ensure client has correct callback URL """ if "callbacks" not in client or client["callbacks"] != [callback_url]: - self.auth0.clients.update( + self.admin_client.clients.update( client["client_id"], { # Overwrite any other callback URL specified @@ -79,7 +81,7 @@ def _ensure_client_logout_url(self, client, logout_url): if "allowed_logout_urls" not in client or client["allowed_logout_urls"] != [ logout_url ]: - self.auth0.clients.update( + self.admin_client.clients.update( client["client_id"], { # Overwrite any other logout URL - users should only land on @@ -91,7 +93,7 @@ def _ensure_client_logout_url(self, client, logout_url): def ensure_client( self, name, callback_url, logout_url, connection_name, connection_config ): - current_clients = self.get_clients() + current_clients = self._get_clients() if name not in current_clients: # Create the client, all good client = self.create_client(name, callback_url, logout_url) @@ -100,7 +102,7 @@ def ensure_client( self._ensure_client_callback(client, callback_url) self._ensure_client_logout_url(client, logout_url) - current_connections = self.get_connections() + current_connections = self._get_connections() if connection_name == "password": # Users should not be shared between hubs - each hub @@ -111,7 +113,7 @@ def ensure_client( if db_connection_name not in current_connections: # connection doesn't exist yet, create it - connection = self.auth0.connections.create( + connection = self.admin_client.connections.create( { "name": db_connection_name, "display_name": name, @@ -138,13 +140,13 @@ def ensure_client( needs_update = True if needs_update: - self.auth0.connections.update( + self.admin_client.connections.update( connection["id"], {"enabled_clients": enabled_clients} ) return client - def get_client_creds(self, client, connection_name): + def get_client_creds(self, client, connection_name, callback_url=None): """ Return z2jh config for auth0 authentication for this JupyterHub """ From c04965c486fdc142801d94a1e76682a9888dbf63 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Sat, 12 Mar 2022 01:28:43 +0200 Subject: [PATCH 07/70] Create the appropriate client provider based on config --- deployer/deploy_actions.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/deployer/deploy_actions.py b/deployer/deploy_actions.py index 0a5b123e38..714cd7bfd1 100644 --- a/deployer/deploy_actions.py +++ b/deployer/deploy_actions.py @@ -8,7 +8,8 @@ from ruamel.yaml import YAML -from auth import KeyProvider +from auth import Auth0ClientProvider +from cilogon_auth import CILogonClientProvider from cluster import Cluster from utils import print_colour from file_acquisition import find_absolute_path_to_cluster_file, get_decrypted_file @@ -172,14 +173,20 @@ def deploy(cluster_name, hub_name, skip_hub_health_test, config_path): with open(decrypted_file_path) as f: config = yaml.load(f) - # All our hubs use Auth0 for Authentication. This lets us programmatically - # determine what auth provider each hub uses - GitHub, Google, etc. Without - # this, we'd have to manually generate credentials for each hub - and we - # don't want to do that. Auth0 domains are tied to a account, and - # this is our auth0 domain for the paid account that 2i2c has. + # Some of our hubs use Auth0 for Authentication and some use CILogong. + # This lets us programmatically determine what auth provider each hub + # uses - GitHub, Google, etc or which Institutional Identity Provider. + # Without this, we'd have to manually generate credentials for each + # hub - and we don't want to do that. Auth0 domains are tied to a account, + # and this is our auth0 domain for the paid account that 2i2c has. auth0 = config["auth0"] + cilogon_admin = config["cilogon_admin"] - k = KeyProvider(auth0["domain"], auth0["client_id"], auth0["client_secret"]) + def _get_client_provider(hub_config): + if hub_config["auth0"].get("enabled", True): + return Auth0ClientProvider(auth0["client_id"], auth0["client_secret"], auth0["domain"]) + + return CILogonClientProvider(cilogon_admin["client_id"], cilogon_admin["client_secret"]) # Each hub needs a unique proxy.secretToken. However, we don't want # to manually generate & save it. We also don't want it to change with @@ -200,11 +207,13 @@ def deploy(cluster_name, hub_name, skip_hub_health_test, config_path): hubs = cluster.hubs if hub_name: hub = next((hub for hub in hubs if hub.spec["name"] == hub_name), None) + client_provider = _get_client_provider(hub.spec) print_colour(f"Deploying hub {hub.spec['name']}...") - hub.deploy(k, SECRET_KEY, skip_hub_health_test) + hub.deploy(client_provider, SECRET_KEY, skip_hub_health_test) else: for i, hub in enumerate(hubs): print_colour( f"{i+1} / {len(hubs)}: Deploying hub {hub.spec['name']}..." ) - hub.deploy(k, SECRET_KEY, skip_hub_health_test) + client_provider = _get_client_provider(hub.spec) + hub.deploy(client_provider, SECRET_KEY, skip_hub_health_test) From a7d10e488ecac6b2fcfc0ac9e03d8475e78edc9e Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Sat, 12 Mar 2022 01:31:19 +0200 Subject: [PATCH 08/70] Update generated config to suport cilogon --- deployer/hub.py | 70 +++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/deployer/hub.py b/deployer/hub.py index 1b35742ee6..036a72692c 100644 --- a/deployer/hub.py +++ b/deployer/hub.py @@ -12,7 +12,7 @@ from textwrap import dedent from ruamel.yaml import YAML -from auth import KeyProvider +from auth_client_provider import ClientProvider from utils import print_colour from file_acquisition import get_decrypted_file, get_decrypted_files @@ -30,7 +30,7 @@ def __init__(self, cluster, spec): self.cluster = cluster self.spec = spec - def get_generated_config(self, auth_provider: KeyProvider, secret_key): + def get_generated_config(self, auth_provider: ClientProvider, secret_key): """ Generate config automatically for each hub @@ -120,33 +120,47 @@ def get_generated_config(self, auth_provider: KeyProvider, secret_key): }, }, } - # - # Allow explicilty ignoring auth0 setup + + # Allow explicilty ignoring auth0/cilogon setup + if self.spec["auth0"].get("enabled", False) and self.spec["cilogon"].get("enabled", False): + return self.apply_hub_helm_chart_fixes(generated_config, secret_key) + + # Send users back to this URL after they authenticate + callback_url = f"https://{self.spec['domain']}/hub/oauth_callback" + # Users are redirected to this URL after they log out + logout_url = f"https://{self.spec['domain']}" + # Human readable name of the client OAuth2 application + client_name = f"{self.cluster.spec['name']}-{self.spec['name']}" + connection_config = None + if self.spec["auth0"].get("enabled", True): - # Auth0 sends users back to this URL after they authenticate - callback_url = f"https://{self.spec['domain']}/hub/oauth_callback" - # Users are redirected to this URL after they log out - logout_url = f"https://{self.spec['domain']}" - client = auth_provider.ensure_client( - name=self.spec["auth0"].get( - "application_name", - f"{self.cluster.spec['name']}-{self.spec['name']}", - ), - callback_url=callback_url, - logout_url=logout_url, - connection_name=self.spec["auth0"]["connection"], - connection_config=self.spec["auth0"].get( - self.spec["auth0"]["connection"], {} - ), - ) - # NOTE: Some dictionary merging might make these lines prettier/more readable. - # Since Auth0 is enabled, we set the authenticator_class to the Auth0OAuthenticator class - generated_config["jupyterhub"]["hub"]["config"]["JupyterHub"] = { - "authenticator_class": "oauthenticator.auth0.Auth0OAuthenticator" - } - generated_config["jupyterhub"]["hub"]["config"][ - "Auth0OAuthenticator" - ] = auth_provider.get_client_creds(client, self.spec["auth0"]["connection"]) + connection_name = self.spec["auth0"]["connection"] + connection_config = self.spec["auth0"].get( + self.spec["auth0"]["connection"], {} + ) + authenticator_class_entrypoint = "auth0" + authenticator_class_name = "Auth0OAuthenticator" + elif self.spec["cilogon"].get("enabled", True): + connection_name = self.spec["cilogon"]["allowed_idps"] + connection_config = self.cluster.config_path.joinpath("enc-cilogon-clients.secret.yaml") + authenticator_class_entrypoint = "cilogon" + authenticator_class_name = "CILogonOAuthenticator" + + client = auth_provider.ensure_client( + name=client_name, + callback_url=callback_url, + logout_url=logout_url, + connection_name=connection_name, + connection_config=connection_config, + ) + + # NOTE: Some dictionary merging might make these lines prettier/more readable. + generated_config["jupyterhub"]["hub"]["config"]["JupyterHub"] = { + "authenticator_class": authenticator_class_entrypoint + } + generated_config["jupyterhub"]["hub"]["config"][ + authenticator_class_name + ] = auth_provider.get_client_creds(client, connection_name, callback_url) return self.apply_hub_helm_chart_fixes(generated_config, secret_key) From 7027cf779334d3211c9bc3f23bef21dfa30d9f26 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Sat, 12 Mar 2022 01:33:59 +0200 Subject: [PATCH 09/70] Working verison of the CILogon client provider --- deployer/cilogon_auth.py | 97 ++++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index f960bfe321..5ca1bf1ece 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -1,7 +1,14 @@ -from yarl import URL - import base64 import requests +import subprocess + +from ruamel.yaml import YAML +from yarl import URL + +from auth_client_provider import ClientProvider +from file_acquisition import get_decrypted_file + +yaml = YAML(typ="safe") class CILogonAdmin: timeout = 5.0 @@ -10,11 +17,12 @@ def __init__(self, admin_id, admin_secret): self.admin_id = admin_id self.admin_secret = admin_secret - bearer_token = base64.urlsafe_b64encode(f"{self.admin_id}:{self.admin_secret}") + token_string = f"{self.admin_id}:{self.admin_secret}" + bearer_token = base64.urlsafe_b64encode(token_string.encode('utf-8')).decode('ascii') self.base_headers = { - 'Authorization': f'Bearer {bearer_token}', - 'Content-Type': 'application/json', + "Authorization": f"Bearer {bearer_token}", + "Content-Type": "application/json; charset=UTF-8", } @@ -33,7 +41,7 @@ def _post(self, url, data=None): return requests.post(url, json=data, headers=headers, timeout=self.timeout) def _get(self, url, params=None): - # todo: make this resilient, make use of exponentiall_backoff + # todo: make this resilient, use of an exponentiall backoff headers = self.base_headers.copy() return requests.get(url, params=params, headers=headers, timeout=self.timeout) @@ -52,7 +60,11 @@ def create(self, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-post.sh """ - return self._post(self._url(), data=body) + response = self._post(self._url(), data=body) + if response.status_code != 200: + return + + return response.json() def get(self, id): """Retrieves a client by its id. @@ -63,7 +75,11 @@ def get(self, id): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-get.sh """ - return self.client.get(self._url(id)) + response = self._get(self._url(id)) + if response.status_code != 200: + return + + return response.json() def update(self, id, body): """Modifies a client by its id. @@ -76,17 +92,20 @@ def update(self, id, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-put.sh """ + response = self._put(self._url(id), data=body) + if response.status_code != 200: + return - return self.client.put(self._url(id), data=body) + return response.json() -class ClientProvider: +class CILogonClientProvider(ClientProvider): def __init__(self, admin_id, admin_secret): self.admin_id = admin_id self.admin_secret = admin_secret @property - def cilogon(self): + def admin_client(self): """ Return a CILogonAdmin instance """ @@ -103,11 +122,9 @@ def create_client(self, name, callback_url): "scope": "openid email org.cilogon.userinfo", } - created_client = self.cilogon.create(client_details) - - return created_client + return self.admin_client.create(client_details) - def update_client(self, name, callback_url): + def update_client(self, client_id, name, callback_url): client_details = { "client_name": name, "app_type": "web", @@ -115,24 +132,45 @@ def update_client(self, name, callback_url): "scope": "openid email org.cilogon.userinfo", } - created_client = self.cilogon.update(client_details) - - return created_client + return self.admin_client.update(client_id, client_details) - def ensure_client(self, name, callback_url): - client = self.get(name) - - if client is None or client["registration_client_uri"] is None: - # Create the client, all good - client = self.create_client(name, callback_url) - else: - # Update the client - client = self.update_client(name, callback_url) + def ensure_client( + self, name, callback_url, logout_url=None, connection_name=None, connection_config=None + ): + client_id = None + try: + with get_decrypted_file(connection_config) as decrypted_path: + with open(decrypted_path) as f: + cilogon_clients = yaml.load(f) + cilogon_client= cilogon_clients.get(name, None) + client_id = cilogon_client["client_id"] + except FileNotFoundError: + cilogon_clients = {} + client_id = None + finally: + if client_id is None: + # Create the client, all good + client = self.create_client(name, callback_url) + cilogon_clients[name] = { + "client_id": client["client_id"], + "client_secret": client["client_id"] + } + # persist the client id + # todo: use a semaphore when we'll deploy hubs in parallel + with open(connection_config, "w+") as f: + yaml.dump(cilogon_clients, f) + subprocess.check_call( + ["sops", "--encrypt", "--in-place", connection_config] + ) + else: + client = self.update_client(client_id, name, callback_url) + # CILogon doesn't return the client secret after its creation + client["client_secret"] = cilogon_clients[name]["client_secret"] return client - def get_client_creds(self, client, connection_name): + def get_client_creds(self, client, connection_name, callback_url): """ Return z2jh config for cilogon authentication for this JupyterHub """ @@ -140,8 +178,9 @@ def get_client_creds(self, client, connection_name): auth = { "client_id": client["client_id"], "client_secret": client["client_secret"], - "username_claim": "nickname" if connection_name == "github" else "email", + "username_claim": "email", "scope": client["scope"], + "oauth_callback_url": callback_url, "logout_redirect_url": "https://cilogon.org/logout/", "allowed_idps": [connection_name] } From 63e7ab90bdce6dfe09f630fc8d85f8d6d469e818 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Sat, 12 Mar 2022 01:34:42 +0200 Subject: [PATCH 10/70] Add encrypted file storing first hubs client and secret --- .../2i2c/enc-cilogon-clients.secret.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/clusters/2i2c/enc-cilogon-clients.secret.yaml diff --git a/config/clusters/2i2c/enc-cilogon-clients.secret.yaml b/config/clusters/2i2c/enc-cilogon-clients.secret.yaml new file mode 100644 index 0000000000..fffeef2746 --- /dev/null +++ b/config/clusters/2i2c/enc-cilogon-clients.secret.yaml @@ -0,0 +1,16 @@ +2i2c-dask-staging: + client_id: ENC[AES256_GCM,data:62jlv1ohUBWdjbxwBslrB42qZ1ro4G47gfn73rvQtGEeKfkMwGzRPdcyRAs4vutQpcU2,iv:rToNLBylM9bYLGyWdSwK4lZCviNc/MG+xIbS2zrThIc=,tag:Q9SlvH/K/ZwQwvz56abqAw==,type:str] + client_secret: ENC[AES256_GCM,data:QMu2sNmmklcq2n7ueF/YaO5rPOZkCKJZO5r2ZN65SpSAo2vsjR4pUB77tLYHQlvJEuRq,iv:jG0UXcXqiicMcmdR1HE+l74iezZOMer6dmTQGvB52YY=,tag:o612E1LCfD+XoElQS3J9oA==,type:str] +sops: + kms: [] + gcp_kms: + - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs + created_at: '2022-03-11T23:29:58Z' + enc: CiQA4OM7eN4xZU214Oj2lVwAZZ1DuSFdg4F06VNV9070TrPpuoUSSQDm5XgWmZErMJf+ITK9028+Y6/CD996qb+MGPJbu6PgPfkGC5ny3tfI6c9ILGCwCzHqbCce9LLJH7Qi9LBlk3lEoro63nx5qI8= + azure_kv: [] + hc_vault: [] + lastmodified: '2022-03-11T23:29:59Z' + mac: ENC[AES256_GCM,data:Rjcjxhxa5lxwD2Vq/NVMwVS8mDIjZQzfxO5S02NP9RpMRAnDhrCEPzNf8+rzYSCMtENX2KA1pGSJ6+TGvE+V817acqA+5LpdVMrChDeDkXrHVpuPayaw/IgOmauQgXAKJGgXaR7x4wdSF0qVum+iq206uX4QtEqTlEQ5S44i7S8=,iv:bGHzP1eRtjF7CryVXZXtim6fd6o2p3bbo6YVWoglkhU=,tag:Pbzp0ejEhXXfI7Fm0MImoA==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.6.1 From d007c77baec2f78959d738835471066bcc288062 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Mar 2022 23:40:04 +0000 Subject: [PATCH 11/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/auth_client_provider.py | 19 ++++++++++------- deployer/cilogon_auth.py | 35 ++++++++++++++++++-------------- deployer/deploy_actions.py | 8 ++++++-- deployer/hub.py | 12 +++++++---- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/deployer/auth_client_provider.py b/deployer/auth_client_provider.py index 69be78ee28..b9bab2006d 100644 --- a/deployer/auth_client_provider.py +++ b/deployer/auth_client_provider.py @@ -15,27 +15,32 @@ def create_client(self, name, callback_url, logout_url=None): Args: name (str): human readable name of the client - callback_url (str): URL that is invoked after OAuth authorization + callback_url (str): URL that is invoked after OAuth authorization (optional) logout_url (string): URL to redirect users to after app logout """ pass def ensure_client( - self, name, callback_url, logout_url=None, connection_name=None, connection_config=None + self, + name, + callback_url, + logout_url=None, + connection_name=None, + connection_config=None, ): """Ensures a client with the specified name and config exists. If one doesn't exist, create it and if it does exist, update it with the specified config. - + **Subclasses must define this method** Args: name (str): human readable name of the client - callback_url (str): URL that is invoked after OAuth authorization + callback_url (str): URL that is invoked after OAuth authorization (optional) logout_url (string): URL to redirect users to after app logout (optional) connection_name: The name of the connection/identity provider (optional) connection_config: Extra configuration to be passed to the chosen connection - + Should return a dict describing the client created/updated. """ @@ -46,8 +51,8 @@ def get_client_creds(self, client, connection_name=None, callback_url=None): Args: client (dict): OAuth2 clientvv - must include `client_id` and `client_secret` keys - (optional) callback_url (str): URL that is invoked after OAuth authorization + (optional) callback_url (str): URL that is invoked after OAuth authorization (optional) connection_name: The name of the connection/identity provider """ - pass \ No newline at end of file + pass diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 5ca1bf1ece..d4b8a71798 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -10,6 +10,7 @@ yaml = YAML(typ="safe") + class CILogonAdmin: timeout = 5.0 @@ -18,13 +19,14 @@ def __init__(self, admin_id, admin_secret): self.admin_secret = admin_secret token_string = f"{self.admin_id}:{self.admin_secret}" - bearer_token = base64.urlsafe_b64encode(token_string.encode('utf-8')).decode('ascii') + bearer_token = base64.urlsafe_b64encode(token_string.encode("utf-8")).decode( + "ascii" + ) self.base_headers = { "Authorization": f"Bearer {bearer_token}", "Content-Type": "application/json; charset=UTF-8", } - def _url(self, id=None): url = "https://cilogon.org/oauth2/oidc-cm" @@ -34,23 +36,22 @@ def _url(self, id=None): return str(URL(url).with_query({"client_id": id})) - def _post(self, url, data=None): headers = self.base_headers.copy() return requests.post(url, json=data, headers=headers, timeout=self.timeout) - + def _get(self, url, params=None): # todo: make this resilient, use of an exponentiall backoff headers = self.base_headers.copy() return requests.get(url, params=params, headers=headers, timeout=self.timeout) - + def _put(self, url, data=None): headers = self.base_headers.copy() return requests.put(url, json=data, headers=headers, timeout=self.timeout) - + def create(self, body): """Creates a new client @@ -65,7 +66,7 @@ def create(self, body): return return response.json() - + def get(self, id): """Retrieves a client by its id. @@ -80,10 +81,10 @@ def get(self, id): return return response.json() - + def update(self, id, body): """Modifies a client by its id. - + The client_secret attribute cannot be updated. Note that any values missing will be deleted from the information for the server! Args: @@ -123,7 +124,7 @@ def create_client(self, name, callback_url): } return self.admin_client.create(client_details) - + def update_client(self, client_id, name, callback_url): client_details = { "client_name": name, @@ -134,16 +135,20 @@ def update_client(self, client_id, name, callback_url): return self.admin_client.update(client_id, client_details) - def ensure_client( - self, name, callback_url, logout_url=None, connection_name=None, connection_config=None + self, + name, + callback_url, + logout_url=None, + connection_name=None, + connection_config=None, ): client_id = None try: with get_decrypted_file(connection_config) as decrypted_path: with open(decrypted_path) as f: cilogon_clients = yaml.load(f) - cilogon_client= cilogon_clients.get(name, None) + cilogon_client = cilogon_clients.get(name, None) client_id = cilogon_client["client_id"] except FileNotFoundError: cilogon_clients = {} @@ -154,7 +159,7 @@ def ensure_client( client = self.create_client(name, callback_url) cilogon_clients[name] = { "client_id": client["client_id"], - "client_secret": client["client_id"] + "client_secret": client["client_id"], } # persist the client id # todo: use a semaphore when we'll deploy hubs in parallel @@ -182,7 +187,7 @@ def get_client_creds(self, client, connection_name, callback_url): "scope": client["scope"], "oauth_callback_url": callback_url, "logout_redirect_url": "https://cilogon.org/logout/", - "allowed_idps": [connection_name] + "allowed_idps": [connection_name], } return auth diff --git a/deployer/deploy_actions.py b/deployer/deploy_actions.py index 714cd7bfd1..b94a913a1d 100644 --- a/deployer/deploy_actions.py +++ b/deployer/deploy_actions.py @@ -184,9 +184,13 @@ def deploy(cluster_name, hub_name, skip_hub_health_test, config_path): def _get_client_provider(hub_config): if hub_config["auth0"].get("enabled", True): - return Auth0ClientProvider(auth0["client_id"], auth0["client_secret"], auth0["domain"]) + return Auth0ClientProvider( + auth0["client_id"], auth0["client_secret"], auth0["domain"] + ) - return CILogonClientProvider(cilogon_admin["client_id"], cilogon_admin["client_secret"]) + return CILogonClientProvider( + cilogon_admin["client_id"], cilogon_admin["client_secret"] + ) # Each hub needs a unique proxy.secretToken. However, we don't want # to manually generate & save it. We also don't want it to change with diff --git a/deployer/hub.py b/deployer/hub.py index 036a72692c..383f9d136a 100644 --- a/deployer/hub.py +++ b/deployer/hub.py @@ -122,7 +122,9 @@ def get_generated_config(self, auth_provider: ClientProvider, secret_key): } # Allow explicilty ignoring auth0/cilogon setup - if self.spec["auth0"].get("enabled", False) and self.spec["cilogon"].get("enabled", False): + if self.spec["auth0"].get("enabled", False) and self.spec["cilogon"].get( + "enabled", False + ): return self.apply_hub_helm_chart_fixes(generated_config, secret_key) # Send users back to this URL after they authenticate @@ -136,13 +138,15 @@ def get_generated_config(self, auth_provider: ClientProvider, secret_key): if self.spec["auth0"].get("enabled", True): connection_name = self.spec["auth0"]["connection"] connection_config = self.spec["auth0"].get( - self.spec["auth0"]["connection"], {} - ) + self.spec["auth0"]["connection"], {} + ) authenticator_class_entrypoint = "auth0" authenticator_class_name = "Auth0OAuthenticator" elif self.spec["cilogon"].get("enabled", True): connection_name = self.spec["cilogon"]["allowed_idps"] - connection_config = self.cluster.config_path.joinpath("enc-cilogon-clients.secret.yaml") + connection_config = self.cluster.config_path.joinpath( + "enc-cilogon-clients.secret.yaml" + ) authenticator_class_entrypoint = "cilogon" authenticator_class_name = "CILogonOAuthenticator" From fd5f18d81979c06138b3b23b5ef952b5d927c5d4 Mon Sep 17 00:00:00 2001 From: Georgiana Elena Date: Tue, 15 Mar 2022 11:29:18 +0200 Subject: [PATCH 12/70] Update deployer/deploy_actions.py Co-authored-by: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> --- deployer/deploy_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployer/deploy_actions.py b/deployer/deploy_actions.py index b94a913a1d..023d21176f 100644 --- a/deployer/deploy_actions.py +++ b/deployer/deploy_actions.py @@ -173,7 +173,7 @@ def deploy(cluster_name, hub_name, skip_hub_health_test, config_path): with open(decrypted_file_path) as f: config = yaml.load(f) - # Some of our hubs use Auth0 for Authentication and some use CILogong. + # Some of our hubs use Auth0 for Authentication and some use CILogon. # This lets us programmatically determine what auth provider each hub # uses - GitHub, Google, etc or which Institutional Identity Provider. # Without this, we'd have to manually generate credentials for each From d517f2cb03d46ff6e67bfe52484a9415cb9bc657 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 15 Mar 2022 15:09:55 +0200 Subject: [PATCH 13/70] Make allowed_idps a list --- config/clusters/2i2c/cluster.yaml | 3 ++- deployer/cilogon_auth.py | 2 +- shared/deployer/cluster.schema.yaml | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index ea90ee86e8..8cbf0e5318 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -35,7 +35,8 @@ hubs: # connection: google-oauth2 cilogon: enabled: true - allowed_idps: "2i2c.org" + allowed_idps: + - "2i2c.org" helm_chart_values_files: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index d4b8a71798..818e8cff8f 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -187,7 +187,7 @@ def get_client_creds(self, client, connection_name, callback_url): "scope": client["scope"], "oauth_callback_url": callback_url, "logout_redirect_url": "https://cilogon.org/logout/", - "allowed_idps": [connection_name], + "allowed_idps": connection_name, } return auth diff --git a/shared/deployer/cluster.schema.yaml b/shared/deployer/cluster.schema.yaml index b1a1dd866e..ceb2e53fb6 100644 --- a/shared/deployer/cluster.schema.yaml +++ b/shared/deployer/cluster.schema.yaml @@ -277,9 +277,10 @@ properties: Whether or not to enable CILogon authentication for this hub. Defaults to false allowed_idps: - type: string + type: array description: | - Allowed identity providers. + A list of identity providers that are allowed to authenticate. + Reference https://github.com/jupyterhub/oauthenticator/blob/main/oauthenticator/cilogon.py#L89-L92 if: properties: enabled: From 1343e88916c58966bd655114d8fe03dcb409b047 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 15 Mar 2022 15:22:02 +0200 Subject: [PATCH 14/70] Raise errors if functions we call and for which we assume certain params are not implemented --- deployer/auth_client_provider.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/deployer/auth_client_provider.py b/deployer/auth_client_provider.py index b9bab2006d..dbaf69f62e 100644 --- a/deployer/auth_client_provider.py +++ b/deployer/auth_client_provider.py @@ -8,7 +8,7 @@ def admin_client(self): pass - def create_client(self, name, callback_url, logout_url=None): + def create_client(self, name, callback_url, logout_url): """Create a OAuth2 client application. **Subclasses must define this method** @@ -16,7 +16,7 @@ def create_client(self, name, callback_url, logout_url=None): Args: name (str): human readable name of the client callback_url (str): URL that is invoked after OAuth authorization - (optional) logout_url (string): URL to redirect users to after app logout + logout_url (string): URL to redirect users to after app logout """ pass @@ -24,9 +24,9 @@ def ensure_client( self, name, callback_url, - logout_url=None, - connection_name=None, - connection_config=None, + logout_url, + connection_name, + connection_config, ): """Ensures a client with the specified name and config exists. If one doesn't exist, create it and if it does exist, update it @@ -37,22 +37,22 @@ def ensure_client( Args: name (str): human readable name of the client callback_url (str): URL that is invoked after OAuth authorization - (optional) logout_url (string): URL to redirect users to after app logout - (optional) connection_name: The name of the connection/identity provider - (optional) connection_config: Extra configuration to be passed to the chosen connection + logout_url (string): URL to redirect users to after app logout + connection_name: The name of the connection/identity provider + connection_config: Extra configuration to be passed to the chosen connection Should return a dict describing the client created/updated. """ - pass + raise NotImplementedError - def get_client_creds(self, client, connection_name=None, callback_url=None): + def get_client_creds(self, client, connection_name, callback_url): """Return z2jh config for auth0 authentication for this JupyterHub. Args: client (dict): OAuth2 clientvv - must include `client_id` and `client_secret` keys - (optional) callback_url (str): URL that is invoked after OAuth authorization - (optional) connection_name: The name of the connection/identity provider + callback_url (str): URL that is invoked after OAuth authorization + connection_name: The name of the connection/identity provider or a list of """ - pass + raise NotImplementedError From 708a45533e01622d1765d8a4d48aea40ffbe69de Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 15 Mar 2022 16:24:15 +0200 Subject: [PATCH 15/70] Rename to reflect there can be multiple connections now that we have cilogon --- deployer/auth.py | 12 +++++++----- deployer/auth_client_provider.py | 12 +++++++----- deployer/cilogon_auth.py | 14 +++++--------- deployer/hub.py | 9 +++++---- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/deployer/auth.py b/deployer/auth.py index b6c38c6379..be15b4b2b1 100644 --- a/deployer/auth.py +++ b/deployer/auth.py @@ -91,8 +91,11 @@ def _ensure_client_logout_url(self, client, logout_url): ) def ensure_client( - self, name, callback_url, logout_url, connection_name, connection_config + self, name, callback_url, logout_url, allowed_connections: str, connection_config ): + # Our Auth0 setup currently suports only one connection, so this must be a string! + connection_name = allowed_connections + current_clients = self._get_clients() if name not in current_clients: # Create the client, all good @@ -146,10 +149,9 @@ def ensure_client( return client - def get_client_creds(self, client, connection_name, callback_url=None): - """ - Return z2jh config for auth0 authentication for this JupyterHub - """ + def get_client_creds(self, client, allowed_connections: str, callback_url): + # Our Auth0 setup currently suports only one connection, so this must be a string! + connection_name = allowed_connections logout_redirect_params = { "client_id": client["client_id"], diff --git a/deployer/auth_client_provider.py b/deployer/auth_client_provider.py index dbaf69f62e..4f62c84efb 100644 --- a/deployer/auth_client_provider.py +++ b/deployer/auth_client_provider.py @@ -25,7 +25,7 @@ def ensure_client( name, callback_url, logout_url, - connection_name, + allowed_connections, connection_config, ): """Ensures a client with the specified name and config exists. @@ -38,21 +38,23 @@ def ensure_client( name (str): human readable name of the client callback_url (str): URL that is invoked after OAuth authorization logout_url (string): URL to redirect users to after app logout - connection_name: The name of the connection/identity provider - connection_config: Extra configuration to be passed to the chosen connection + allowed_connections: A single string or a list of strings representing the + connections/identity providers the client should accept + connection_config: Extra configuration to be passed to the chosen connection/s Should return a dict describing the client created/updated. """ raise NotImplementedError - def get_client_creds(self, client, connection_name, callback_url): + def get_client_creds(self, client, allowed_connections, callback_url): """Return z2jh config for auth0 authentication for this JupyterHub. Args: client (dict): OAuth2 clientvv - must include `client_id` and `client_secret` keys callback_url (str): URL that is invoked after OAuth authorization - connection_name: The name of the connection/identity provider or a list of + allowed_connections: A single string or a list of strings representing the + allowed connections/identity providers """ raise NotImplementedError diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 818e8cff8f..688d37a933 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -139,9 +139,9 @@ def ensure_client( self, name, callback_url, - logout_url=None, - connection_name=None, - connection_config=None, + logout_url, + allowed_connections, + connection_config, ): client_id = None try: @@ -175,11 +175,7 @@ def ensure_client( return client - def get_client_creds(self, client, connection_name, callback_url): - """ - Return z2jh config for cilogon authentication for this JupyterHub - """ - + def get_client_creds(self, client, allowed_connections, callback_url): auth = { "client_id": client["client_id"], "client_secret": client["client_secret"], @@ -187,7 +183,7 @@ def get_client_creds(self, client, connection_name, callback_url): "scope": client["scope"], "oauth_callback_url": callback_url, "logout_redirect_url": "https://cilogon.org/logout/", - "allowed_idps": connection_name, + "allowed_idps": allowed_connections, } return auth diff --git a/deployer/hub.py b/deployer/hub.py index 383f9d136a..1d464cd6f5 100644 --- a/deployer/hub.py +++ b/deployer/hub.py @@ -136,14 +136,15 @@ def get_generated_config(self, auth_provider: ClientProvider, secret_key): connection_config = None if self.spec["auth0"].get("enabled", True): - connection_name = self.spec["auth0"]["connection"] + # With auth0 we can only have one accepted connection github/google-oauth2/password + allowed_connections = self.spec["auth0"]["connection"] connection_config = self.spec["auth0"].get( self.spec["auth0"]["connection"], {} ) authenticator_class_entrypoint = "auth0" authenticator_class_name = "Auth0OAuthenticator" elif self.spec["cilogon"].get("enabled", True): - connection_name = self.spec["cilogon"]["allowed_idps"] + allowed_connections = self.spec["cilogon"]["allowed_idps"] connection_config = self.cluster.config_path.joinpath( "enc-cilogon-clients.secret.yaml" ) @@ -154,7 +155,7 @@ def get_generated_config(self, auth_provider: ClientProvider, secret_key): name=client_name, callback_url=callback_url, logout_url=logout_url, - connection_name=connection_name, + allowed_connections=allowed_connections, connection_config=connection_config, ) @@ -164,7 +165,7 @@ def get_generated_config(self, auth_provider: ClientProvider, secret_key): } generated_config["jupyterhub"]["hub"]["config"][ authenticator_class_name - ] = auth_provider.get_client_creds(client, connection_name, callback_url) + ] = auth_provider.get_client_creds(client, allowed_connections, callback_url) return self.apply_hub_helm_chart_fixes(generated_config, secret_key) From 9b4f48cbe0bd273cb16243c7ce672091bfb10d66 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:13:27 +0000 Subject: [PATCH 16/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/auth.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deployer/auth.py b/deployer/auth.py index be15b4b2b1..b48edaab78 100644 --- a/deployer/auth.py +++ b/deployer/auth.py @@ -91,7 +91,12 @@ def _ensure_client_logout_url(self, client, logout_url): ) def ensure_client( - self, name, callback_url, logout_url, allowed_connections: str, connection_config + self, + name, + callback_url, + logout_url, + allowed_connections: str, + connection_config, ): # Our Auth0 setup currently suports only one connection, so this must be a string! connection_name = allowed_connections From 6623c9c28cb75d4b944f7520a4f60b7e3aac4e51 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 15 Mar 2022 18:50:38 +0200 Subject: [PATCH 17/70] Fix storing client_secret bug --- config/clusters/2i2c/enc-cilogon-clients.secret.yaml | 12 ++++++------ deployer/cilogon_auth.py | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/config/clusters/2i2c/enc-cilogon-clients.secret.yaml b/config/clusters/2i2c/enc-cilogon-clients.secret.yaml index fffeef2746..578a00f23d 100644 --- a/config/clusters/2i2c/enc-cilogon-clients.secret.yaml +++ b/config/clusters/2i2c/enc-cilogon-clients.secret.yaml @@ -1,16 +1,16 @@ 2i2c-dask-staging: - client_id: ENC[AES256_GCM,data:62jlv1ohUBWdjbxwBslrB42qZ1ro4G47gfn73rvQtGEeKfkMwGzRPdcyRAs4vutQpcU2,iv:rToNLBylM9bYLGyWdSwK4lZCviNc/MG+xIbS2zrThIc=,tag:Q9SlvH/K/ZwQwvz56abqAw==,type:str] - client_secret: ENC[AES256_GCM,data:QMu2sNmmklcq2n7ueF/YaO5rPOZkCKJZO5r2ZN65SpSAo2vsjR4pUB77tLYHQlvJEuRq,iv:jG0UXcXqiicMcmdR1HE+l74iezZOMer6dmTQGvB52YY=,tag:o612E1LCfD+XoElQS3J9oA==,type:str] + client_id: ENC[AES256_GCM,data:vLfNQZDk50MZIMD0mxYfFldvzrWWfPxXkd4OGL+h0iXImpZt3s6BABhfOxQihEPTzPeC,iv:ZQTdFkSrmi2AnBcjYTKwxYMorE3gIY1zopiW4hlgt8Y=,tag:x2w6P5T7qjnHHy4V/Pr3XQ==,type:str] + client_secret: ENC[AES256_GCM,data:mM/Lv83aaFeiLQkPggk3xf2a6R2uZqPA0BQSOoGcrgSXGjA7c+HwJYCuXTLqhn1vW+NoA6M2qIncMQZz/FtPPapgO4UusfhO6tVjmpwA+65tEYxw+1Y=,iv:h03nIKVzw7E/pHLowvnN3zmhcU3MikIMuTjm+pvBBbc=,tag:68vu2Ha694dWMC8+1+9uPA==,type:str] sops: kms: [] gcp_kms: - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs - created_at: '2022-03-11T23:29:58Z' - enc: CiQA4OM7eN4xZU214Oj2lVwAZZ1DuSFdg4F06VNV9070TrPpuoUSSQDm5XgWmZErMJf+ITK9028+Y6/CD996qb+MGPJbu6PgPfkGC5ny3tfI6c9ILGCwCzHqbCce9LLJH7Qi9LBlk3lEoro63nx5qI8= + created_at: '2022-03-15T15:21:52Z' + enc: CiQA4OM7eA0JlEpdqC4dmIWrRGbKY27aXBlfXQsHzkZAyrUdO/cSSQDm5XgWWUEs8/ZsJ7/4bq9CW/n+a+UZ1FLt9NTdWJFNZsATRUEbrCy2r+HZsC06nRWnN5pEVg32dZtjUhX76RMZESKuv8ku2Xw= azure_kv: [] hc_vault: [] - lastmodified: '2022-03-11T23:29:59Z' - mac: ENC[AES256_GCM,data:Rjcjxhxa5lxwD2Vq/NVMwVS8mDIjZQzfxO5S02NP9RpMRAnDhrCEPzNf8+rzYSCMtENX2KA1pGSJ6+TGvE+V817acqA+5LpdVMrChDeDkXrHVpuPayaw/IgOmauQgXAKJGgXaR7x4wdSF0qVum+iq206uX4QtEqTlEQ5S44i7S8=,iv:bGHzP1eRtjF7CryVXZXtim6fd6o2p3bbo6YVWoglkhU=,tag:Pbzp0ejEhXXfI7Fm0MImoA==,type:str] + lastmodified: '2022-03-15T15:21:52Z' + mac: ENC[AES256_GCM,data:ipX8xUR0753NgDJUSpE0cPv4I7Tb2y6v+3zR52BD734TQsyg5aLFnhhhcTCDTEeUbg0/8Z41HWMRghIMq8G3cHV88Hdwvu+lec9bVPKvIyHONnYr8fhKl+7HrO5hT7ArboU8bZmyC3a4odnZKMhIeIGjvNx9EiZU3Kx6NopcKmk=,iv:j/l0u7Gfl6OeRqr//95S1sattB4MCfD2r0Hf/4pT5qs=,tag:t6FYkmDFdlXPMq3pLfz61Q==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.6.1 diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 688d37a933..b80af09766 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -1,4 +1,5 @@ import base64 +import time import requests import subprocess @@ -42,7 +43,6 @@ def _post(self, url, data=None): return requests.post(url, json=data, headers=headers, timeout=self.timeout) def _get(self, url, params=None): - # todo: make this resilient, use of an exponentiall backoff headers = self.base_headers.copy() return requests.get(url, params=params, headers=headers, timeout=self.timeout) @@ -157,9 +157,10 @@ def ensure_client( if client_id is None: # Create the client, all good client = self.create_client(name, callback_url) + print(f"Created a new CILogon client for {name}.") cilogon_clients[name] = { "client_id": client["client_id"], - "client_secret": client["client_id"], + "client_secret": client["client_secret"], } # persist the client id # todo: use a semaphore when we'll deploy hubs in parallel @@ -169,6 +170,7 @@ def ensure_client( ["sops", "--encrypt", "--in-place", connection_config] ) else: + print(f"Updated the existing CILogon client for {name}.") client = self.update_client(client_id, name, callback_url) # CILogon doesn't return the client secret after its creation client["client_secret"] = cilogon_clients[name]["client_secret"] From 0fd22dbd8cb283d283e8957830bc9ebd9de27012 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 15 Mar 2022 18:54:37 +0200 Subject: [PATCH 18/70] Try multiple times to get details about a client if not successful --- deployer/cilogon_auth.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index b80af09766..999a3e2e1f 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -45,7 +45,20 @@ def _post(self, url, data=None): def _get(self, url, params=None): headers = self.base_headers.copy() - return requests.get(url, params=params, headers=headers, timeout=self.timeout) + timeout = time.time() + 10 + t = 0.05 + + # Allow up to 10s of retrying getting the response + while time.time() < timeout: + response = requests.get(url, params=params, headers=headers, timeout=self.timeout) + if response.status_code == 200: + return response + + t = min(2, t * 2) + time.sleep(t) + + # Return latest response to the request + return response def _put(self, url, data=None): headers = self.base_headers.copy() From 4f73b46f276a9a79f39e04c7145c0f500c62234c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 16:55:06 +0000 Subject: [PATCH 19/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 999a3e2e1f..a14585eabf 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -50,7 +50,9 @@ def _get(self, url, params=None): # Allow up to 10s of retrying getting the response while time.time() < timeout: - response = requests.get(url, params=params, headers=headers, timeout=self.timeout) + response = requests.get( + url, params=params, headers=headers, timeout=self.timeout + ) if response.status_code == 200: return response From 4c8df9ba022f080c1abf36d4fb5a76342c03229c Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 15:30:44 +0200 Subject: [PATCH 20/70] Get rid of the generic client provider --- deployer/auth_client_provider.py | 60 -------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 deployer/auth_client_provider.py diff --git a/deployer/auth_client_provider.py b/deployer/auth_client_provider.py deleted file mode 100644 index 4f62c84efb..0000000000 --- a/deployer/auth_client_provider.py +++ /dev/null @@ -1,60 +0,0 @@ -class ClientProvider: - @property - def admin_client(self): - """Return an administrative instance of the authentication client. - - This instance must be able to create/update/delete other clients. - """ - - pass - - def create_client(self, name, callback_url, logout_url): - """Create a OAuth2 client application. - - **Subclasses must define this method** - - Args: - name (str): human readable name of the client - callback_url (str): URL that is invoked after OAuth authorization - logout_url (string): URL to redirect users to after app logout - """ - pass - - def ensure_client( - self, - name, - callback_url, - logout_url, - allowed_connections, - connection_config, - ): - """Ensures a client with the specified name and config exists. - If one doesn't exist, create it and if it does exist, update it - with the specified config. - - **Subclasses must define this method** - - Args: - name (str): human readable name of the client - callback_url (str): URL that is invoked after OAuth authorization - logout_url (string): URL to redirect users to after app logout - allowed_connections: A single string or a list of strings representing the - connections/identity providers the client should accept - connection_config: Extra configuration to be passed to the chosen connection/s - - Should return a dict describing the client created/updated. - - """ - raise NotImplementedError - - def get_client_creds(self, client, allowed_connections, callback_url): - """Return z2jh config for auth0 authentication for this JupyterHub. - - Args: - client (dict): OAuth2 clientvv - must include `client_id` and `client_secret` keys - callback_url (str): URL that is invoked after OAuth authorization - allowed_connections: A single string or a list of strings representing the - allowed connections/identity providers - """ - - raise NotImplementedError From db2200cdae6d920c3ff8b8db7f372ef07bbc6250 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 15:35:48 +0200 Subject: [PATCH 21/70] Auth0 client provider isn't based on the generic provider anymore --- deployer/auth.py | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/deployer/auth.py b/deployer/auth.py index b48edaab78..e9112801d5 100644 --- a/deployer/auth.py +++ b/deployer/auth.py @@ -4,8 +4,6 @@ from auth0.v3.management import Auth0 from yarl import URL -from auth_client_provider import ClientProvider - # What key in the authenticated user's profile to use as hub username # This shouldn't be changeable by the user! USERNAME_KEYS = { @@ -16,36 +14,36 @@ } -class Auth0ClientProvider(ClientProvider): - def __init__(self, client_id, client_secret, domain): +class KeyProvider(): + def __init__(self, domain, client_id, client_secret): self.client_id = client_id self.client_secret = client_secret self.domain = domain @property - def admin_client(self): + def auth0(self): """ Return an authenticated Auth0 instance """ - if not hasattr(self, "_admin_client"): + if not hasattr(self, "_auth0"): gt = GetToken(self.domain) creds = gt.client_credentials( self.client_id, self.client_secret, f"https://{self.domain}/api/v2/" ) - self._admin_client = Auth0(self.domain, creds["access_token"]) - return self._admin_client + self._auth0 = Auth0(self.domain, creds["access_token"]) + return self._auth0 def _get_clients(self): return { client["name"]: client # Our account is limited to 100 clients, and we want it all in one go - for client in self.admin_client.clients.all(per_page=100) + for client in self.auth0.clients.all(per_page=100) } def _get_connections(self): return { connection["name"]: connection - for connection in self.admin_client.connections.all() + for connection in self.auth0.connections.all() } def create_client(self, name, callback_url, logout_url): @@ -55,7 +53,7 @@ def create_client(self, name, callback_url, logout_url): "callbacks": [callback_url], "allowed_logout_urls": [logout_url], } - created_client = self.admin_client.clients.create(client) + created_client = self.auth0.clients.create(client) return created_client def _ensure_client_callback(self, client, callback_url): @@ -63,7 +61,7 @@ def _ensure_client_callback(self, client, callback_url): Ensure client has correct callback URL """ if "callbacks" not in client or client["callbacks"] != [callback_url]: - self.admin_client.clients.update( + self.auth0.clients.update( client["client_id"], { # Overwrite any other callback URL specified @@ -81,7 +79,7 @@ def _ensure_client_logout_url(self, client, logout_url): if "allowed_logout_urls" not in client or client["allowed_logout_urls"] != [ logout_url ]: - self.admin_client.clients.update( + self.auth0.clients.update( client["client_id"], { # Overwrite any other logout URL - users should only land on @@ -95,12 +93,9 @@ def ensure_client( name, callback_url, logout_url, - allowed_connections: str, + connection_name: str, connection_config, ): - # Our Auth0 setup currently suports only one connection, so this must be a string! - connection_name = allowed_connections - current_clients = self._get_clients() if name not in current_clients: # Create the client, all good @@ -121,7 +116,7 @@ def ensure_client( if db_connection_name not in current_connections: # connection doesn't exist yet, create it - connection = self.admin_client.connections.create( + connection = self.auth0.connections.create( { "name": db_connection_name, "display_name": name, @@ -148,16 +143,13 @@ def ensure_client( needs_update = True if needs_update: - self.admin_client.connections.update( + self.auth0.connections.update( connection["id"], {"enabled_clients": enabled_clients} ) return client - def get_client_creds(self, client, allowed_connections: str, callback_url): - # Our Auth0 setup currently suports only one connection, so this must be a string! - connection_name = allowed_connections - + def get_client_creds(self, client, connection_name: str, callback_url): logout_redirect_params = { "client_id": client["client_id"], "returnTo": client["allowed_logout_urls"][0], From b2f067e2f5be9260a0c89346ace834cf96418656 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 15:39:40 +0200 Subject: [PATCH 22/70] Restore previous schema file --- shared/deployer/cluster.schema.yaml | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/shared/deployer/cluster.schema.yaml b/shared/deployer/cluster.schema.yaml index ceb2e53fb6..5f1da81615 100644 --- a/shared/deployer/cluster.schema.yaml +++ b/shared/deployer/cluster.schema.yaml @@ -241,6 +241,13 @@ properties: Authentication method users of the hub can use to log in to the hub. We support a subset of the [connectors](https://auth0.com/docs/identityproviders) that auth0 supports + application_name: + type: string + description: | + We make use of an OAuth2 applications in Auth0 and this is the + name of that application. Defaults to + "-". + See https://manage.auth0.com/dashboard/us/2i2c/applications. password: type: object description: | @@ -262,25 +269,6 @@ properties: then: required: - connection - cilogon: - additionalProperties: false - type: object - description: | - Some hubs use CILogon for authentication, and we dynamically fetch the credentials - needed for each hub - client_id, client_secret, callback_url - on deploy. This - block contains configuration on how cilogon should be configured for this hub. - properties: - enabled: - type: boolean - default: false - description: | - Whether or not to enable CILogon authentication for this hub. - Defaults to false - allowed_idps: - type: array - description: | - A list of identity providers that are allowed to authenticate. - Reference https://github.com/jupyterhub/oauthenticator/blob/main/oauthenticator/cilogon.py#L89-L92 if: properties: enabled: From b994c1b61ba2aa51ae41758bb941581ff557d71f Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 15:41:10 +0200 Subject: [PATCH 23/70] Remove cilogon setup from the hub module --- deployer/hub.py | 73 ++++++++++++++++++------------------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/deployer/hub.py b/deployer/hub.py index 1d464cd6f5..1b35742ee6 100644 --- a/deployer/hub.py +++ b/deployer/hub.py @@ -12,7 +12,7 @@ from textwrap import dedent from ruamel.yaml import YAML -from auth_client_provider import ClientProvider +from auth import KeyProvider from utils import print_colour from file_acquisition import get_decrypted_file, get_decrypted_files @@ -30,7 +30,7 @@ def __init__(self, cluster, spec): self.cluster = cluster self.spec = spec - def get_generated_config(self, auth_provider: ClientProvider, secret_key): + def get_generated_config(self, auth_provider: KeyProvider, secret_key): """ Generate config automatically for each hub @@ -120,52 +120,33 @@ def get_generated_config(self, auth_provider: ClientProvider, secret_key): }, }, } - - # Allow explicilty ignoring auth0/cilogon setup - if self.spec["auth0"].get("enabled", False) and self.spec["cilogon"].get( - "enabled", False - ): - return self.apply_hub_helm_chart_fixes(generated_config, secret_key) - - # Send users back to this URL after they authenticate - callback_url = f"https://{self.spec['domain']}/hub/oauth_callback" - # Users are redirected to this URL after they log out - logout_url = f"https://{self.spec['domain']}" - # Human readable name of the client OAuth2 application - client_name = f"{self.cluster.spec['name']}-{self.spec['name']}" - connection_config = None - + # + # Allow explicilty ignoring auth0 setup if self.spec["auth0"].get("enabled", True): - # With auth0 we can only have one accepted connection github/google-oauth2/password - allowed_connections = self.spec["auth0"]["connection"] - connection_config = self.spec["auth0"].get( - self.spec["auth0"]["connection"], {} - ) - authenticator_class_entrypoint = "auth0" - authenticator_class_name = "Auth0OAuthenticator" - elif self.spec["cilogon"].get("enabled", True): - allowed_connections = self.spec["cilogon"]["allowed_idps"] - connection_config = self.cluster.config_path.joinpath( - "enc-cilogon-clients.secret.yaml" + # Auth0 sends users back to this URL after they authenticate + callback_url = f"https://{self.spec['domain']}/hub/oauth_callback" + # Users are redirected to this URL after they log out + logout_url = f"https://{self.spec['domain']}" + client = auth_provider.ensure_client( + name=self.spec["auth0"].get( + "application_name", + f"{self.cluster.spec['name']}-{self.spec['name']}", + ), + callback_url=callback_url, + logout_url=logout_url, + connection_name=self.spec["auth0"]["connection"], + connection_config=self.spec["auth0"].get( + self.spec["auth0"]["connection"], {} + ), ) - authenticator_class_entrypoint = "cilogon" - authenticator_class_name = "CILogonOAuthenticator" - - client = auth_provider.ensure_client( - name=client_name, - callback_url=callback_url, - logout_url=logout_url, - allowed_connections=allowed_connections, - connection_config=connection_config, - ) - - # NOTE: Some dictionary merging might make these lines prettier/more readable. - generated_config["jupyterhub"]["hub"]["config"]["JupyterHub"] = { - "authenticator_class": authenticator_class_entrypoint - } - generated_config["jupyterhub"]["hub"]["config"][ - authenticator_class_name - ] = auth_provider.get_client_creds(client, allowed_connections, callback_url) + # NOTE: Some dictionary merging might make these lines prettier/more readable. + # Since Auth0 is enabled, we set the authenticator_class to the Auth0OAuthenticator class + generated_config["jupyterhub"]["hub"]["config"]["JupyterHub"] = { + "authenticator_class": "oauthenticator.auth0.Auth0OAuthenticator" + } + generated_config["jupyterhub"]["hub"]["config"][ + "Auth0OAuthenticator" + ] = auth_provider.get_client_creds(client, self.spec["auth0"]["connection"]) return self.apply_hub_helm_chart_fixes(generated_config, secret_key) From fe76db64c7b04f35321ec189e0aa4a7ea9fc5d7d Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 15:42:45 +0200 Subject: [PATCH 24/70] Remove cilogon setup from the deployer --- deployer/deploy_actions.py | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/deployer/deploy_actions.py b/deployer/deploy_actions.py index 023d21176f..0a5b123e38 100644 --- a/deployer/deploy_actions.py +++ b/deployer/deploy_actions.py @@ -8,8 +8,7 @@ from ruamel.yaml import YAML -from auth import Auth0ClientProvider -from cilogon_auth import CILogonClientProvider +from auth import KeyProvider from cluster import Cluster from utils import print_colour from file_acquisition import find_absolute_path_to_cluster_file, get_decrypted_file @@ -173,24 +172,14 @@ def deploy(cluster_name, hub_name, skip_hub_health_test, config_path): with open(decrypted_file_path) as f: config = yaml.load(f) - # Some of our hubs use Auth0 for Authentication and some use CILogon. - # This lets us programmatically determine what auth provider each hub - # uses - GitHub, Google, etc or which Institutional Identity Provider. - # Without this, we'd have to manually generate credentials for each - # hub - and we don't want to do that. Auth0 domains are tied to a account, - # and this is our auth0 domain for the paid account that 2i2c has. + # All our hubs use Auth0 for Authentication. This lets us programmatically + # determine what auth provider each hub uses - GitHub, Google, etc. Without + # this, we'd have to manually generate credentials for each hub - and we + # don't want to do that. Auth0 domains are tied to a account, and + # this is our auth0 domain for the paid account that 2i2c has. auth0 = config["auth0"] - cilogon_admin = config["cilogon_admin"] - def _get_client_provider(hub_config): - if hub_config["auth0"].get("enabled", True): - return Auth0ClientProvider( - auth0["client_id"], auth0["client_secret"], auth0["domain"] - ) - - return CILogonClientProvider( - cilogon_admin["client_id"], cilogon_admin["client_secret"] - ) + k = KeyProvider(auth0["domain"], auth0["client_id"], auth0["client_secret"]) # Each hub needs a unique proxy.secretToken. However, we don't want # to manually generate & save it. We also don't want it to change with @@ -211,13 +200,11 @@ def _get_client_provider(hub_config): hubs = cluster.hubs if hub_name: hub = next((hub for hub in hubs if hub.spec["name"] == hub_name), None) - client_provider = _get_client_provider(hub.spec) print_colour(f"Deploying hub {hub.spec['name']}...") - hub.deploy(client_provider, SECRET_KEY, skip_hub_health_test) + hub.deploy(k, SECRET_KEY, skip_hub_health_test) else: for i, hub in enumerate(hubs): print_colour( f"{i+1} / {len(hubs)}: Deploying hub {hub.spec['name']}..." ) - client_provider = _get_client_provider(hub.spec) - hub.deploy(client_provider, SECRET_KEY, skip_hub_health_test) + hub.deploy(k, SECRET_KEY, skip_hub_health_test) From a816b07cfcdb3ededb01d47bd083f37c299bad9f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 13:44:46 +0000 Subject: [PATCH 25/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployer/auth.py b/deployer/auth.py index e9112801d5..c84cab6d87 100644 --- a/deployer/auth.py +++ b/deployer/auth.py @@ -14,7 +14,7 @@ } -class KeyProvider(): +class KeyProvider: def __init__(self, domain, client_id, client_secret): self.client_id = client_id self.client_secret = client_secret From a1c736a964e2ea97d508c1975d4c237fb9c3c2d5 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 15:45:05 +0200 Subject: [PATCH 26/70] Remove type hits and fix class --- deployer/auth.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/deployer/auth.py b/deployer/auth.py index c84cab6d87..4a768a6c2b 100644 --- a/deployer/auth.py +++ b/deployer/auth.py @@ -93,7 +93,7 @@ def ensure_client( name, callback_url, logout_url, - connection_name: str, + connection_name, connection_config, ): current_clients = self._get_clients() @@ -149,7 +149,10 @@ def ensure_client( return client - def get_client_creds(self, client, connection_name: str, callback_url): + def get_client_creds(self, client, connection_name): + """ + Return z2jh config for auth0 authentication for this JupyterHub + """ logout_redirect_params = { "client_id": client["client_id"], "returnTo": client["allowed_logout_urls"][0], From 68b38f81770da1d517c32f441fbb8fcf76534d42 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 15:48:04 +0200 Subject: [PATCH 27/70] Fix up any remaining bits of the old setup --- shared/deployer/cluster.schema.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/shared/deployer/cluster.schema.yaml b/shared/deployer/cluster.schema.yaml index 5f1da81615..5e91dee904 100644 --- a/shared/deployer/cluster.schema.yaml +++ b/shared/deployer/cluster.schema.yaml @@ -247,6 +247,7 @@ properties: We make use of an OAuth2 applications in Auth0 and this is the name of that application. Defaults to "-". + See https://manage.auth0.com/dashboard/us/2i2c/applications. password: type: object @@ -269,13 +270,6 @@ properties: then: required: - connection - if: - properties: - enabled: - const: true - then: - required: - - allowed_idps helm_chart_values_files: type: array description: | From 9bc6bf7bb708432e16871668d5c661fd87fafdae Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 18:56:45 +0200 Subject: [PATCH 28/70] Example of new CILogon config --- config/clusters/2i2c/dask-staging.values.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/clusters/2i2c/dask-staging.values.yaml b/config/clusters/2i2c/dask-staging.values.yaml index 4ecc41f012..58fdfea86c 100644 --- a/config/clusters/2i2c/dask-staging.values.yaml +++ b/config/clusters/2i2c/dask-staging.values.yaml @@ -31,7 +31,8 @@ basehub: tag: 2021.02.19 hub: config: - Authenticator: - allowed_users: &dask_staging_users - - colliand@gmail.com - admin_users: *dask_staging_users + CILogonOAuthenticator: + username_claim: "email", + scope: "openid email org.cilogon.userinfo", + oauth_callback_url: "https://dask-staging.pilot.2i2c.cloud/hub/oauth_callback" + allowed_idps: "2i2c.org", From 54febf3f2e3a3588c61bd41c5afc107dedda3c0e Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 18:57:16 +0200 Subject: [PATCH 29/70] Turn the cilogon client provider into a script --- deployer/cilogon_auth.py | 197 ++++++++++++++++++++++++++------------- 1 file changed, 133 insertions(+), 64 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index a14585eabf..bc2e9f91be 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -1,13 +1,12 @@ +import argparse import base64 import time import requests import subprocess - from ruamel.yaml import YAML from yarl import URL -from auth_client_provider import ClientProvider -from file_acquisition import get_decrypted_file +from file_acquisition import find_absolute_path_to_cluster_file, get_decrypted_file yaml = YAML(typ="safe") @@ -76,7 +75,10 @@ def create(self, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-post.sh """ + print(f"body {body}") response = self._post(self._url(), data=body) + print(f"response {response}") + print(response.json()) if response.status_code != 200: return @@ -115,7 +117,7 @@ def update(self, id, body): return response.json() -class CILogonClientProvider(ClientProvider): +class CILogonClientProvider: def __init__(self, admin_id, admin_secret): self.admin_id = admin_id self.admin_secret = admin_secret @@ -130,77 +132,144 @@ def admin_client(self): return self._cilogon_admin - def create_client(self, name, callback_url): + def _build_client_details(self, cluster_name, hub_name, callback_url): client_details = { - "client_name": name, + "client_name": f"{cluster_name}-{hub_name}", "app_type": "web", "redirect_uris": [callback_url], "scope": "openid email org.cilogon.userinfo", } - return self.admin_client.create(client_details) + return client_details - def update_client(self, client_id, name, callback_url): - client_details = { - "client_name": name, - "app_type": "web", - "redirect_uris": [callback_url], - "scope": "openid email org.cilogon.userinfo", + def _build_config_filename(self, cluster_name, hub_name): + cluster_config_dir_path = find_absolute_path_to_cluster_file(cluster_name).parent + + return cluster_config_dir_path.joinpath(f"enc-{hub_name}.secret.values.yaml") + + def _persist_client_credentials(self, client, hub_type, config_filename): + auth_config = {} + jupyterhub_config = { + "jupyterhub": { + "hub": { + "config": { + "CILogonOAuthenticator": { + "client_id": client["client_id"], + "client_secret": client["client_secret"] + } + } + } + } } - return self.admin_client.update(client_id, client_details) + if hub_type != "basehub": + auth_config["basehub"] = jupyterhub_config + else: + auth_config = jupyterhub_config - def ensure_client( - self, - name, - callback_url, - logout_url, - allowed_connections, - connection_config, - ): - client_id = None + with open(config_filename, "w+") as f: + yaml.dump(auth_config, f) + subprocess.check_call( + ["sops", "--encrypt", "--in-place", config_filename] + ) + + def _load_client_id(self, config_filename): try: - with get_decrypted_file(connection_config) as decrypted_path: + with get_decrypted_file(config_filename) as decrypted_path: with open(decrypted_path) as f: - cilogon_clients = yaml.load(f) - cilogon_client = cilogon_clients.get(name, None) - client_id = cilogon_client["client_id"] + auth_config = yaml.load(f) + + basehub = auth_config.get("basehub", None) + if basehub: + return auth_config["basehub"]["jupyterhub"]["hub"]["config"]["CILogonOAuthenticator"]["client_id"] + return auth_config["jupyterhub"]["hub"]["config"]["CILogonOAuthenticator"]["client_id"] except FileNotFoundError: - cilogon_clients = {} - client_id = None - finally: - if client_id is None: - # Create the client, all good - client = self.create_client(name, callback_url) - print(f"Created a new CILogon client for {name}.") - cilogon_clients[name] = { - "client_id": client["client_id"], - "client_secret": client["client_secret"], - } - # persist the client id - # todo: use a semaphore when we'll deploy hubs in parallel - with open(connection_config, "w+") as f: - yaml.dump(cilogon_clients, f) - subprocess.check_call( - ["sops", "--encrypt", "--in-place", connection_config] - ) - else: - print(f"Updated the existing CILogon client for {name}.") - client = self.update_client(client_id, name, callback_url) - # CILogon doesn't return the client secret after its creation - client["client_secret"] = cilogon_clients[name]["client_secret"] - - return client - - def get_client_creds(self, client, allowed_connections, callback_url): - auth = { - "client_id": client["client_id"], - "client_secret": client["client_secret"], - "username_claim": "email", - "scope": client["scope"], - "oauth_callback_url": callback_url, - "logout_redirect_url": "https://cilogon.org/logout/", - "allowed_idps": allowed_connections, - } + print("The CILogon client you requested to update doesn't exist! Please create it first.") + + + def create_client(self, cluster_name, hub_name, hub_type, callback_url): + client_details = self._build_client_details(cluster_name, hub_name, callback_url) + config_filename = self._build_config_filename(cluster_name, hub_name) + + # Ask CILogon to create the client + print(f"Creating client with details {client_details}") + client = self.admin_client.create(client_details) + print(f"Created a new CILogon client for {cluster_name}-{hub_name}.") + print(client) + + # Persist and encrypt the client credentials + self._persist_client_credentials(client, hub_type, config_filename) + print(f"Client credentials encrypted and stored to {config_filename}.") - return auth + def update_client(self, cluster_name, hub_name, callback_url): + client_details = self._build_client_details(cluster_name, hub_name, callback_url) + config_filename = self._build_config_filename(cluster_name, hub_name) + client_id = self.load_client_id(config_filename) + + print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}.") + return self.admin_client.update(client_id, client_details) + + +def main(): + argparser = argparse.ArgumentParser( + description="""A command line tool to create/update/delete + CILogon clients. + """ + ) + subparsers = argparser.add_subparsers( + required=True, dest="action", help="Available subcommands" + ) + + # Create subcommand + create_parser = subparsers.add_parser( + "create", + help="Create a CILogon client", + ) + + create_parser.add_argument( + "cluster_name", + type=str, + help="The name of the cluster where the hub lives", + ) + + create_parser.add_argument( + "hub_name", + type=str, + help="The hub for which we'll create a CILogon client", + ) + + create_parser.add_argument( + "hub_type", + type=str, + help="The type of hub for which we'll create a CILogon client.", + default="basehub" + ) + + create_parser.add_argument( + "callback_url", + type=str, + help="URL that is invoked after OAuth authorization", + ) + + args = argparser.parse_args() + + # This filepath is relative to the PROJECT ROOT + general_auth_config = "shared/deployer/enc-auth-providers-credentials.secret.yaml" + with get_decrypted_file(general_auth_config) as decrypted_file_path: + with open(decrypted_file_path) as f: + config = yaml.load(f) + + cilogon = CILogonClientProvider(config["cilogon"]["client_id"], config["cilogon"]["client_secret"]) + print(cilogon.admin_id) + print(cilogon.admin_secret) + + # if args.action == "create": + # cilogon.create_client( + # args.cluster_name, + # args.hub_name, + # args.hub_type, + # args.callback_url, + # ) + +if __name__ == "__main__": + main() From 9fe31d18ef9126ac6625af0f92b38c100fea4e60 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:57:44 +0000 Subject: [PATCH 30/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_auth.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index bc2e9f91be..70833cc46e 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -143,7 +143,9 @@ def _build_client_details(self, cluster_name, hub_name, callback_url): return client_details def _build_config_filename(self, cluster_name, hub_name): - cluster_config_dir_path = find_absolute_path_to_cluster_file(cluster_name).parent + cluster_config_dir_path = find_absolute_path_to_cluster_file( + cluster_name + ).parent return cluster_config_dir_path.joinpath(f"enc-{hub_name}.secret.values.yaml") @@ -155,7 +157,7 @@ def _persist_client_credentials(self, client, hub_type, config_filename): "config": { "CILogonOAuthenticator": { "client_id": client["client_id"], - "client_secret": client["client_secret"] + "client_secret": client["client_secret"], } } } @@ -169,9 +171,7 @@ def _persist_client_credentials(self, client, hub_type, config_filename): with open(config_filename, "w+") as f: yaml.dump(auth_config, f) - subprocess.check_call( - ["sops", "--encrypt", "--in-place", config_filename] - ) + subprocess.check_call(["sops", "--encrypt", "--in-place", config_filename]) def _load_client_id(self, config_filename): try: @@ -181,14 +181,21 @@ def _load_client_id(self, config_filename): basehub = auth_config.get("basehub", None) if basehub: - return auth_config["basehub"]["jupyterhub"]["hub"]["config"]["CILogonOAuthenticator"]["client_id"] - return auth_config["jupyterhub"]["hub"]["config"]["CILogonOAuthenticator"]["client_id"] + return auth_config["basehub"]["jupyterhub"]["hub"]["config"][ + "CILogonOAuthenticator" + ]["client_id"] + return auth_config["jupyterhub"]["hub"]["config"]["CILogonOAuthenticator"][ + "client_id" + ] except FileNotFoundError: - print("The CILogon client you requested to update doesn't exist! Please create it first.") - + print( + "The CILogon client you requested to update doesn't exist! Please create it first." + ) def create_client(self, cluster_name, hub_name, hub_type, callback_url): - client_details = self._build_client_details(cluster_name, hub_name, callback_url) + client_details = self._build_client_details( + cluster_name, hub_name, callback_url + ) config_filename = self._build_config_filename(cluster_name, hub_name) # Ask CILogon to create the client @@ -202,7 +209,9 @@ def create_client(self, cluster_name, hub_name, hub_type, callback_url): print(f"Client credentials encrypted and stored to {config_filename}.") def update_client(self, cluster_name, hub_name, callback_url): - client_details = self._build_client_details(cluster_name, hub_name, callback_url) + client_details = self._build_client_details( + cluster_name, hub_name, callback_url + ) config_filename = self._build_config_filename(cluster_name, hub_name) client_id = self.load_client_id(config_filename) @@ -242,7 +251,7 @@ def main(): "hub_type", type=str, help="The type of hub for which we'll create a CILogon client.", - default="basehub" + default="basehub", ) create_parser.add_argument( @@ -259,7 +268,9 @@ def main(): with open(decrypted_file_path) as f: config = yaml.load(f) - cilogon = CILogonClientProvider(config["cilogon"]["client_id"], config["cilogon"]["client_secret"]) + cilogon = CILogonClientProvider( + config["cilogon"]["client_id"], config["cilogon"]["client_secret"] + ) print(cilogon.admin_id) print(cilogon.admin_secret) @@ -271,5 +282,6 @@ def main(): # args.callback_url, # ) + if __name__ == "__main__": main() From b355371a2a2f96e9e3a3d15585af3f08cc0b7196 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Thu, 17 Mar 2022 19:00:58 +0200 Subject: [PATCH 31/70] Remove prints --- deployer/cilogon_auth.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 70833cc46e..fa965394bb 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -271,17 +271,14 @@ def main(): cilogon = CILogonClientProvider( config["cilogon"]["client_id"], config["cilogon"]["client_secret"] ) - print(cilogon.admin_id) - print(cilogon.admin_secret) - - # if args.action == "create": - # cilogon.create_client( - # args.cluster_name, - # args.hub_name, - # args.hub_type, - # args.callback_url, - # ) + if args.action == "create": + cilogon.create_client( + args.cluster_name, + args.hub_name, + args.hub_type, + args.callback_url, + ) if __name__ == "__main__": main() From 432a2e9864d49a8c0f0fa73c2a54db02c1f3b1ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 17:01:26 +0000 Subject: [PATCH 32/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index fa965394bb..29865aac4e 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -280,5 +280,6 @@ def main(): args.callback_url, ) + if __name__ == "__main__": main() From 07fe422d6e5161fe6c53a84db1ef2c088e74a5de Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Fri, 18 Mar 2022 13:14:32 +0200 Subject: [PATCH 33/70] Remove old cilogon config --- config/clusters/2i2c/cluster.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index 8cbf0e5318..298c90ff82 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -33,10 +33,6 @@ hubs: # matching value for jupyterhub.custom.2i2c.add_staff_user_ids_of_type! enabled: false # connection: google-oauth2 - cilogon: - enabled: true - allowed_idps: - - "2i2c.org" helm_chart_values_files: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check From da5868f803fefde84524c3e158266b94ea1ffbbe Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Fri, 18 Mar 2022 13:51:17 +0200 Subject: [PATCH 34/70] Raise http errors --- deployer/cilogon_auth.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 29865aac4e..69563d243c 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -75,14 +75,7 @@ def create(self, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-post.sh """ - print(f"body {body}") - response = self._post(self._url(), data=body) - print(f"response {response}") - print(response.json()) - if response.status_code != 200: - return - - return response.json() + return self._post(self._url(), data=body) def get(self, id): """Retrieves a client by its id. @@ -93,11 +86,7 @@ def get(self, id): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-get.sh """ - response = self._get(self._url(id)) - if response.status_code != 200: - return - - return response.json() + return self._get(self._url(id)) def update(self, id, body): """Modifies a client by its id. @@ -110,11 +99,7 @@ def update(self, id, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-put.sh """ - response = self._put(self._url(id), data=body) - if response.status_code != 200: - return - - return response.json() + return self._put(self._url(id), data=body) class CILogonClientProvider: @@ -200,9 +185,13 @@ def create_client(self, cluster_name, hub_name, hub_type, callback_url): # Ask CILogon to create the client print(f"Creating client with details {client_details}") - client = self.admin_client.create(client_details) + response = self.admin_client.create(client_details) + if response.status_code != 200: + print(f"An error occured when creating the {cluster_name}-{hub_name} client. \n Error was {response.text}.") + response.raise_for_status() + + client = response.json() print(f"Created a new CILogon client for {cluster_name}-{hub_name}.") - print(client) # Persist and encrypt the client credentials self._persist_client_credentials(client, hub_type, config_filename) From 1cc37205e56474985dae16fa4be1c060c0b9e626 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 18 Mar 2022 11:51:42 +0000 Subject: [PATCH 35/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 69563d243c..aa58d614d0 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -187,7 +187,9 @@ def create_client(self, cluster_name, hub_name, hub_type, callback_url): print(f"Creating client with details {client_details}") response = self.admin_client.create(client_details) if response.status_code != 200: - print(f"An error occured when creating the {cluster_name}-{hub_name} client. \n Error was {response.text}.") + print( + f"An error occured when creating the {cluster_name}-{hub_name} client. \n Error was {response.text}." + ) response.raise_for_status() client = response.json() From fe9f2551efd3dcadbe74bea1381cea411688ea7a Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 13:27:31 +0200 Subject: [PATCH 36/70] Use the correct cilogon admin credentials --- deployer/cilogon_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index aa58d614d0..5c8973a87f 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -260,7 +260,7 @@ def main(): config = yaml.load(f) cilogon = CILogonClientProvider( - config["cilogon"]["client_id"], config["cilogon"]["client_secret"] + config["cilogon_admin"]["client_id"], config["cilogon_admin"]["client_secret"] ) if args.action == "create": From e7f3a2caa757752780e38d9e64ef78bd2dcf567f Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 13:36:20 +0200 Subject: [PATCH 37/70] Use the correct yaml syntax --- config/clusters/2i2c/dask-staging.values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/clusters/2i2c/dask-staging.values.yaml b/config/clusters/2i2c/dask-staging.values.yaml index 58fdfea86c..8fc6256b0b 100644 --- a/config/clusters/2i2c/dask-staging.values.yaml +++ b/config/clusters/2i2c/dask-staging.values.yaml @@ -32,7 +32,7 @@ basehub: hub: config: CILogonOAuthenticator: - username_claim: "email", - scope: "openid email org.cilogon.userinfo", + username_claim: "email" + scope: "openid email org.cilogon.userinfo" oauth_callback_url: "https://dask-staging.pilot.2i2c.cloud/hub/oauth_callback" - allowed_idps: "2i2c.org", + allowed_idps: "2i2c.org" From 19da64229a7a9f78b901babd19be833c61f122f6 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 13:58:48 +0200 Subject: [PATCH 38/70] Add error messages and return early if clients exists or no --- deployer/cilogon_auth.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 5c8973a87f..890ae96a96 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -1,8 +1,10 @@ import argparse import base64 -import time import requests import subprocess +import time + +from pathlib import Path from ruamel.yaml import YAML from yarl import URL @@ -183,6 +185,12 @@ def create_client(self, cluster_name, hub_name, hub_type, callback_url): ) config_filename = self._build_config_filename(cluster_name, hub_name) + if Path(config_filename).is_file(): + print( + f"Oops! A CILogon client already exists for this hub! Use the `update` command to update it or delete {config_filename} if you want to generate a new one." + ) + return + # Ask CILogon to create the client print(f"Creating client with details {client_details}") response = self.admin_client.create(client_details) @@ -204,6 +212,14 @@ def update_client(self, cluster_name, hub_name, callback_url): cluster_name, hub_name, callback_url ) config_filename = self._build_config_filename(cluster_name, hub_name) + + if not Path(config_filename).is_file(): + print( + f"Oops! No CILogon client has been found for this hub! Use the `create` command to create one" + ) + return + + client_id = self.load_client_id(config_filename) print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}.") From c23c5745309ee02f896ae4de5d77b8aa2f557778 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Mar 2022 11:59:15 +0000 Subject: [PATCH 39/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 890ae96a96..a4eaaa7f7c 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -219,7 +219,6 @@ def update_client(self, cluster_name, hub_name, callback_url): ) return - client_id = self.load_client_id(config_filename) print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}.") From 7d6fe1e59655fdfed7a5b730fba031e0eb8a48d5 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 15:45:26 +0200 Subject: [PATCH 40/70] Client creds are now stored differently --- .../2i2c/enc-cilogon-clients.secret.yaml | 16 --------------- .../2i2c/enc-dask-staging.secret.values.yaml | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 16 deletions(-) delete mode 100644 config/clusters/2i2c/enc-cilogon-clients.secret.yaml create mode 100644 config/clusters/2i2c/enc-dask-staging.secret.values.yaml diff --git a/config/clusters/2i2c/enc-cilogon-clients.secret.yaml b/config/clusters/2i2c/enc-cilogon-clients.secret.yaml deleted file mode 100644 index 578a00f23d..0000000000 --- a/config/clusters/2i2c/enc-cilogon-clients.secret.yaml +++ /dev/null @@ -1,16 +0,0 @@ -2i2c-dask-staging: - client_id: ENC[AES256_GCM,data:vLfNQZDk50MZIMD0mxYfFldvzrWWfPxXkd4OGL+h0iXImpZt3s6BABhfOxQihEPTzPeC,iv:ZQTdFkSrmi2AnBcjYTKwxYMorE3gIY1zopiW4hlgt8Y=,tag:x2w6P5T7qjnHHy4V/Pr3XQ==,type:str] - client_secret: ENC[AES256_GCM,data:mM/Lv83aaFeiLQkPggk3xf2a6R2uZqPA0BQSOoGcrgSXGjA7c+HwJYCuXTLqhn1vW+NoA6M2qIncMQZz/FtPPapgO4UusfhO6tVjmpwA+65tEYxw+1Y=,iv:h03nIKVzw7E/pHLowvnN3zmhcU3MikIMuTjm+pvBBbc=,tag:68vu2Ha694dWMC8+1+9uPA==,type:str] -sops: - kms: [] - gcp_kms: - - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs - created_at: '2022-03-15T15:21:52Z' - enc: CiQA4OM7eA0JlEpdqC4dmIWrRGbKY27aXBlfXQsHzkZAyrUdO/cSSQDm5XgWWUEs8/ZsJ7/4bq9CW/n+a+UZ1FLt9NTdWJFNZsATRUEbrCy2r+HZsC06nRWnN5pEVg32dZtjUhX76RMZESKuv8ku2Xw= - azure_kv: [] - hc_vault: [] - lastmodified: '2022-03-15T15:21:52Z' - mac: ENC[AES256_GCM,data:ipX8xUR0753NgDJUSpE0cPv4I7Tb2y6v+3zR52BD734TQsyg5aLFnhhhcTCDTEeUbg0/8Z41HWMRghIMq8G3cHV88Hdwvu+lec9bVPKvIyHONnYr8fhKl+7HrO5hT7ArboU8bZmyC3a4odnZKMhIeIGjvNx9EiZU3Kx6NopcKmk=,iv:j/l0u7Gfl6OeRqr//95S1sattB4MCfD2r0Hf/4pT5qs=,tag:t6FYkmDFdlXPMq3pLfz61Q==,type:str] - pgp: [] - unencrypted_suffix: _unencrypted - version: 3.6.1 diff --git a/config/clusters/2i2c/enc-dask-staging.secret.values.yaml b/config/clusters/2i2c/enc-dask-staging.secret.values.yaml new file mode 100644 index 0000000000..1b7298c168 --- /dev/null +++ b/config/clusters/2i2c/enc-dask-staging.secret.values.yaml @@ -0,0 +1,20 @@ +basehub: + jupyterhub: + hub: + config: + CILogonOAuthenticator: + client_id: ENC[AES256_GCM,data:U2tPwhzJNuPaVhZh2+l/x06yu2qIgBPBaQQ+DAIHbTmKoRDGWMtrNWQddmbwKEootGiz,iv:sX1r0qjT6B8qCCEjHA31xwHpQdOJdLyAy8wdig+5CBk=,tag:Ns276qpjnejuv9Ui8yNgSQ==,type:str] + client_secret: ENC[AES256_GCM,data:pMoD+NYq1IEjnl7TZkkJRYVmsAuz0NIbEkbANUTFFtt1FWXxjs2993AnRzNBanL9G7G48udurnpZsRt3Nze2bJIBUulB0AA4pnmKQz1fpERbk2Gi2Yo=,iv:tg5ZUw+N9mxvqw/E8SMyTONjWA8MtBcytKvoOj1ByDg=,tag:/kPamEhUtYUamzk8YrMPTQ==,type:str] +sops: + kms: [] + gcp_kms: + - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs + created_at: '2022-03-21T13:32:55Z' + enc: CiQA4OM7eILHin2dVwbBCzUPzS05fF4t5RZBAE0JfP6atdLuO38SSQDm5XgWkZe3tUEj+YR1BIfS+/loNOIei+7DK1t/wzTd3306YjyW44g7pZdxKgQqw9V26HGR4jpUf/Xnj9fnslwR2yCkt2KPUjs= + azure_kv: [] + hc_vault: [] + lastmodified: '2022-03-21T13:32:56Z' + mac: ENC[AES256_GCM,data:2TB3w10yWfkFC0rIwDnHkDhEZJtC7FClfEQzjFsHnFS6cNcIPZf+mAOMgfipq/PosGNAdGOaTMH95cNXJt4z5sMAoRzFINu5seef3llpax6HtbJ2D4AAcCx7QNq4RGVD1fH2abqZ6P9TxG9VEsHZb0NsaYEA/mhI5kXxrA1fB5s=,iv:4mebKvZWxV1vRW/7f+xRPAU092pb94+0vTfehoNGytw=,tag:nEPCTLbJLPD2D9xGuyW+vQ==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.6.1 From 67786c9d80a083c8d83f2925f90a762b4aec640f Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 15:57:44 +0200 Subject: [PATCH 41/70] Add secrets for the 2i2c staging cilogon client --- .../2i2c/enc-staging.secret.values.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 config/clusters/2i2c/enc-staging.secret.values.yaml diff --git a/config/clusters/2i2c/enc-staging.secret.values.yaml b/config/clusters/2i2c/enc-staging.secret.values.yaml new file mode 100644 index 0000000000..a858020c50 --- /dev/null +++ b/config/clusters/2i2c/enc-staging.secret.values.yaml @@ -0,0 +1,19 @@ +jupyterhub: + hub: + config: + CILogonOAuthenticator: + client_id: ENC[AES256_GCM,data:UH/edYPBsi2V7VvwjNraqePF0j33QdAzByz14+cOVrzaJUhh8uHJI6UGby7/T6JK8WYm,iv:8NMi6r5McwCreuf3pRhsu3ET6cBm6HjNySGnFeiFYy0=,tag:yiIHk+vVw9qpuSClIir1hg==,type:str] + client_secret: ENC[AES256_GCM,data:tFYiFGunRWRzN7u/qalkfL6L+jARTNT42Bollopo6tsG3QFxoGy1Ww4v15Lye/UysVImyirgFT4C6tQXZR462wZdEaEj9o9+1SiUqf0UGAhJFcvoiPI=,iv:6RNRBv9szFszPg4FWyisdZkTTT4pcNixJDoufeOIz7A=,tag:REMhCsHlZPAdiNPWOseIbg==,type:str] +sops: + kms: [] + gcp_kms: + - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs + created_at: '2022-03-21T10:38:36Z' + enc: CiQA4OM7eJn+A9b0ulwu64MnqKfDM1EwtoKzj7Utg4iXOccLCroSSQDm5XgWCwOR2/FDqBIOrVVltPV7nASpq8h+fiHw5dYTSaPUyAMYwQ62iytA2kwTGQcOMmtxZVhn4dpGt2b0VlEvdiHP02Cgzvo= + azure_kv: [] + hc_vault: [] + lastmodified: '2022-03-21T10:38:36Z' + mac: ENC[AES256_GCM,data:D50ijsF6YmlX/El96nrIgxEcEmfbJabVvKIO33zi8PfjqkQZj7L9XdGMz9FzNRvtSu2+PwhZRr+98pqWb4N2SvuVjqPfskJwigVVQifNxOtI2P3V2LvnA/rnYvvTkpfzrcwBJPHsUL8VCAeY8OjxdEpamqFsrlyFG4z2HQ0dAQg=,iv:uDkxaW/3dZZTer1iuqhfIHtZ8vvOY7TCKfFnaI2pcZM=,tag:XjF32PtqZifGwW0LsKa/8Q==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.6.1 From a87bbd3807ec73b27668c581068b4e106cccd3c5 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 17:01:38 +0200 Subject: [PATCH 42/70] Add more info msgs ans subcommands --- deployer/cilogon_auth.py | 128 ++++++++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 15 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index a4eaaa7f7c..0681d02a08 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -77,7 +77,18 @@ def create(self, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-post.sh """ - return self._post(self._url(), data=body) + response = self._post(self._url(), data=body) + client_name = body["client_name"] + + if response.status_code != 200: + print( + f"An error occured when creating the {client_name} client. \n Error was {response.text}." + ) + response.raise_for_status() + + print(f"Successfully created a new CILogon client for {client_name}!") + return response.json() + def get(self, id): """Retrieves a client by its id. @@ -88,7 +99,16 @@ def get(self, id): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-get.sh """ - return self._get(self._url(id)) + response = self._get(self._url(id)) + + if response.status_code != 200: + print( + f"An error occured when creating the {id} client. \n Error was {response.text}." + ) + response.raise_for_status() + + print(f"Successfully created a new CILogon client for {id}!") + return response.json() def update(self, id, body): """Modifies a client by its id. @@ -101,7 +121,17 @@ def update(self, id, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-put.sh """ - return self._put(self._url(id), data=body) + response = self._put(self._url(id), data=body) + client_name = body["client_name"] + + if response.status_code != 200: + print( + f"An error occured when updating the {client_name} client. \n Error was {response.text}." + ) + response.raise_for_status() + + print("Client updated succesfuly!") + return response.status_code class CILogonClientProvider: @@ -187,21 +217,16 @@ def create_client(self, cluster_name, hub_name, hub_type, callback_url): if Path(config_filename).is_file(): print( - f"Oops! A CILogon client already exists for this hub! Use the `update` command to update it or delete {config_filename} if you want to generate a new one." + f""" + Oops! A CILogon client already exists for this hub! + Use the update subcommand to update it or delete {config_filename} if you want to generate a new one. + """ ) return # Ask CILogon to create the client print(f"Creating client with details {client_details}") - response = self.admin_client.create(client_details) - if response.status_code != 200: - print( - f"An error occured when creating the {cluster_name}-{hub_name} client. \n Error was {response.text}." - ) - response.raise_for_status() - - client = response.json() - print(f"Created a new CILogon client for {cluster_name}-{hub_name}.") + client = self.admin_client.create(client_details) # Persist and encrypt the client credentials self._persist_client_credentials(client, hub_type, config_filename) @@ -215,15 +240,31 @@ def update_client(self, cluster_name, hub_name, callback_url): if not Path(config_filename).is_file(): print( - f"Oops! No CILogon client has been found for this hub! Use the `create` command to create one" + f"Oops! No CILogon client has been found for this hub! Use the create subcommand to create one." ) return - client_id = self.load_client_id(config_filename) + + client_id = self._load_client_id(config_filename) print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}.") return self.admin_client.update(client_id, client_details) + def get_client(self, cluster_name, hub_name): + config_filename = self._build_config_filename(cluster_name, hub_name) + + if not Path(config_filename).is_file(): + print( + f"Oops! No CILogon client has been found for this hub! Use the `create` command to create one." + ) + return + + client_id = self._load_client_id(config_filename) + + print(f"Getting the stored CILogon client details for {cluster_name}-{hub_name}...") + response = self.admin_client.get(client_id) + print(response.json()) + def main(): argparser = argparse.ArgumentParser( @@ -266,6 +307,52 @@ def main(): help="URL that is invoked after OAuth authorization", ) + # Update subcommand + update_parser = subparsers.add_parser( + "update", + help="Update a CILogon client", + ) + + update_parser.add_argument( + "cluster_name", + type=str, + help="The name of the cluster where the hub lives", + ) + + update_parser.add_argument( + "hub_name", + type=str, + help="The hub for which we'll update the CILogon client", + ) + + update_parser.add_argument( + "callback_url", + type=str, + help=""" + New callback_url to associate with the client. + This URL is invoked after OAuth authorization + """, + ) + + # Get subcommand + get_parser = subparsers.add_parser( + "get", + help="Retrieve details about an existing CILogon client", + ) + + get_parser.add_argument( + "cluster_name", + type=str, + help="The name of the cluster where the hub lives", + ) + + get_parser.add_argument( + "hub_name", + type=str, + help="The hub for which we'll retrieve the CILogon client details.", + ) + + args = argparser.parse_args() # This filepath is relative to the PROJECT ROOT @@ -285,6 +372,17 @@ def main(): args.hub_type, args.callback_url, ) + elif args.action == "update": + cilogon.update_client( + args.cluster_name, + args.hub_name, + args.callback_url, + ) + elif args.action == "get": + cilogon.get_client( + args.cluster_name, + args.hub_name, + ) if __name__ == "__main__": From f416a5a4ad191f871ea6f58c802c8645a404eedb Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 17:44:42 +0200 Subject: [PATCH 43/70] Update some of the msgs --- deployer/cilogon_auth.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 0681d02a08..8302e3f6c6 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -103,11 +103,11 @@ def get(self, id): if response.status_code != 200: print( - f"An error occured when creating the {id} client. \n Error was {response.text}." + f"An error occured when getting the details of {id} client. \n Error was {response.text}." ) response.raise_for_status() - print(f"Successfully created a new CILogon client for {id}!") + print(f"Successfully got the details for {id} client!") return response.json() def update(self, id, body): @@ -225,7 +225,7 @@ def create_client(self, cluster_name, hub_name, hub_type, callback_url): return # Ask CILogon to create the client - print(f"Creating client with details {client_details}") + print(f"Creating client with details {client_details}...") client = self.admin_client.create(client_details) # Persist and encrypt the client credentials @@ -247,7 +247,7 @@ def update_client(self, cluster_name, hub_name, callback_url): client_id = self._load_client_id(config_filename) - print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}.") + print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}...") return self.admin_client.update(client_id, client_details) def get_client(self, cluster_name, hub_name): @@ -262,8 +262,7 @@ def get_client(self, cluster_name, hub_name): client_id = self._load_client_id(config_filename) print(f"Getting the stored CILogon client details for {cluster_name}-{hub_name}...") - response = self.admin_client.get(client_id) - print(response.json()) + print(self.admin_client.get(client_id)) def main(): From 69188cc451e2d293eb5d94b4a77788a0dce0ea38 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 17:50:21 +0200 Subject: [PATCH 44/70] Correctly enable the cilogon authenticator for dask-staging --- config/clusters/2i2c/cluster.yaml | 1 - config/clusters/2i2c/dask-staging.values.yaml | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index 298c90ff82..f6dc6fbaba 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -32,7 +32,6 @@ hubs: # connection update? Also ensure the basehub Helm chart is provided a # matching value for jupyterhub.custom.2i2c.add_staff_user_ids_of_type! enabled: false - # connection: google-oauth2 helm_chart_values_files: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check diff --git a/config/clusters/2i2c/dask-staging.values.yaml b/config/clusters/2i2c/dask-staging.values.yaml index 8fc6256b0b..f1e0c2e339 100644 --- a/config/clusters/2i2c/dask-staging.values.yaml +++ b/config/clusters/2i2c/dask-staging.values.yaml @@ -31,8 +31,14 @@ basehub: tag: 2021.02.19 hub: config: + JupyterHub: + authenticator_class: cilogon CILogonOAuthenticator: username_claim: "email" - scope: "openid email org.cilogon.userinfo" - oauth_callback_url: "https://dask-staging.pilot.2i2c.cloud/hub/oauth_callback" - allowed_idps: "2i2c.org" + scope: + - openid + - email + - org.cilogon.userinfo + oauth_callback_url: "https://dask-staging.2i2c.cloud/hub/oauth_callback" + allowed_idps: + - "2i2c.org" From 0ed6108fb0dc97d8643a3bd0cea6fe80010302f6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:04:16 +0000 Subject: [PATCH 45/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_auth.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 8302e3f6c6..3106920af6 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -89,7 +89,6 @@ def create(self, body): print(f"Successfully created a new CILogon client for {client_name}!") return response.json() - def get(self, id): """Retrieves a client by its id. @@ -244,7 +243,6 @@ def update_client(self, cluster_name, hub_name, callback_url): ) return - client_id = self._load_client_id(config_filename) print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}...") @@ -261,7 +259,9 @@ def get_client(self, cluster_name, hub_name): client_id = self._load_client_id(config_filename) - print(f"Getting the stored CILogon client details for {cluster_name}-{hub_name}...") + print( + f"Getting the stored CILogon client details for {cluster_name}-{hub_name}..." + ) print(self.admin_client.get(client_id)) @@ -351,7 +351,6 @@ def main(): help="The hub for which we'll retrieve the CILogon client details.", ) - args = argparser.parse_args() # This filepath is relative to the PROJECT ROOT From c0fae585c4953351e889636c2297bf5a4973ce8a Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 18:07:22 +0200 Subject: [PATCH 46/70] Add the cilogon secrets values files to the list of helm charts --- config/clusters/2i2c/cluster.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index f6dc6fbaba..519bcad9bf 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -36,6 +36,7 @@ hubs: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check # that you intend for these files to be applied in this order. + - enc-dask-staging.secret.values.yaml - dask-staging.values.yaml - name: demo display_name: "2i2c demo" From 9e0987c813a3d616f4ed61ddcb9cd29479975564 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 18:09:58 +0200 Subject: [PATCH 47/70] Remove the need for f-strings --- deployer/cilogon_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_auth.py index 3106920af6..a2db943207 100644 --- a/deployer/cilogon_auth.py +++ b/deployer/cilogon_auth.py @@ -239,7 +239,7 @@ def update_client(self, cluster_name, hub_name, callback_url): if not Path(config_filename).is_file(): print( - f"Oops! No CILogon client has been found for this hub! Use the create subcommand to create one." + "Oops! No CILogon client has been found for this hub! Use the create subcommand to create one." ) return @@ -253,7 +253,7 @@ def get_client(self, cluster_name, hub_name): if not Path(config_filename).is_file(): print( - f"Oops! No CILogon client has been found for this hub! Use the `create` command to create one." + "Oops! No CILogon client has been found for this hub! Use the `create` command to create one." ) return From 315e6efe9390121d4a51fd6b1bd6e55fca7e65dd Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 18:27:55 +0200 Subject: [PATCH 48/70] Rename cilogon cmd utility --- deployer/{cilogon_auth.py => cilogon_app.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename deployer/{cilogon_auth.py => cilogon_app.py} (100%) diff --git a/deployer/cilogon_auth.py b/deployer/cilogon_app.py similarity index 100% rename from deployer/cilogon_auth.py rename to deployer/cilogon_app.py From 4da925abdf929000697263bebdad366f533c5fb9 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Mon, 21 Mar 2022 19:00:17 +0200 Subject: [PATCH 49/70] Add some docs --- docs/howto/configure/auth-management.md | 101 +++++++++++++++++++++--- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/docs/howto/configure/auth-management.md b/docs/howto/configure/auth-management.md index 419d77f370..784ccf6221 100644 --- a/docs/howto/configure/auth-management.md +++ b/docs/howto/configure/auth-management.md @@ -193,13 +193,7 @@ See [the GitHub apps for organizations docs](https://docs.github.com/en/organiza ## CILogon -[CILogon](https://www.cilogon.org) is a service provider that allows users to login against various identity providers, including campus identity providers. 2i2c manages CILogon through [auth0](https://auth0.com), similar to Google and GitHub authentication. - -```{seealso} -See the [CILogon documentation on `Auth0`](https://www.cilogon.org/auth0) for more configuration information. -``` - -### Key terms +[CILogon](https://www.cilogon.org) is a service provider that allows users to login against various identity providers, including campus identity providers. 2i2c can manage CILogon either using the JupyterHub CILogonOAuthenticator or through [auth0](https://auth0.com), similar to Google and GitHub authentication. Some key terms about CILogon authentication worth mentioning: @@ -227,7 +221,96 @@ User account The JupyterHub usernames will be the **email address** that users provide when authenticating with an institutional identity provider. It will not be the CILogon `user_id`! This is because the `USERNAME_KEY` used for the CILogon login is the email address. ``` -### Steps to enable CILogon authentication +### JupyterHub CILogonOAuthenticator + +The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar with the ones for enabling GitHubOAuthenticator: + +1. **Create a CILogon OAuth client** + This can be achieved by using the [cilogon_app.py](https://github.com/2i2c-org/infrastructure/blob/master/deployer/cilogon_app.py) script. + + - The script needs to be passed the cluster and hub name for which a client id and secret will be generated, but also the hub type, and the authorisation callback URL. + - The authorisation callback URL is the homepage url appended with `/hub/oauth_callback`. For example, `staging.pilot.2i2c.cloud/hub/oauth_callback`. + - Example script invocation that creates a CILogon OAuth client for the 2i2c dask-staging hub: + ```bash + python3 ./deployer/cilogon_auth.py create 2i2c dask-staging daskhub https://dask-staging.2i2c.cloud/hub/oauth_callback + ``` + - If successfull, the script will have created a secret values file under `config/clusters//enc-.secret.values.yaml`. This file + holds the encrypted OAuth client id and secret that have been created for this hub. + - The unecrypted file contents should look like this: + ```yaml + jupyterhub: + hub: + config: + GitHubOAuthenticator: + client_id: CLIENT_ID + client_secret: CLIENT_SECRET + ``` + +2. **Set the hub to _not_ configure Auth0 in the `config/clusters//cluster.yaml` file.** + To ensure the deployer does not provision and configure an OAuth app from Auth0, the following config should be added to the appropriate hub in the cluster's `cluster.yaml` file. + + ```yaml + hubs: + - name: + auth0: + enabled: false + ``` + +3. **If not already present, add the secret hub config file to the list of helm chart values file in `config/clusters/cluster.yaml`.** + If you created the `enc-.secret.values.yaml` file in step 2, add it the the `cluster.yaml` file like so: + + ```yaml + ... + hubs: + - name: + ... + helm_chart_values_files: + - .values.yaml + - enc-.secret.values.yaml + ... + ``` + +5. **Edit the non-secret config under `config/clusters//.values.yaml`.** + You should make sure the matching hub config takes one of the following forms. + + ```{warning} + When using this method of authentication, make sure to remove the `allowed_users` key from the config. + This is because this key will block any user not listed under it **even if** they are valid members of the the organisation or team you are authenticating against. + + Also, the `admin_users` list need to match `allowed_idps` currently. TODO: we should fix this upstream. + ``` + + To authenticate using CILogon, allowing only a certain identity provider: + + ```yaml + jupyterhub: + hub: + config: + JupyterHub: + authenticator_class: cilogon + CILogonOAuthenticator: + oauth_callback_url: https://{{ HUB_DOMAIN }}/hub/oauth_callback + username_claim: USERNAME_KEY + scope: + - openid + - email + - org.cilogon.userinfo + allowed_idps: + - 2i2c.org + - IDP + ``` + - Check the [CILogon scopes section](https://www.cilogon.org/oidc#h.p_PEQXL8QUjsQm) to checkout available values for `USERNAME_KEY` claim. + - Per [CILogon's suggestion]((https://www.cilogon.org/oidc#h.p_PEQXL8QUjsQm)), please use the same list of scopes in this example when enabling CILogon for a hub. + +6. Run the deployer as normal to apply the config. + + +### CILogon through Auth0 + +```{seealso} +See the [CILogon documentation on `Auth0`](https://www.cilogon.org/auth0) for more configuration information. +``` +The steps to enable the CILogon authentication through Auth0 for a hub are: 1. List CILogon as the type of connection we want for a hub, via `auth0.connection`: @@ -242,7 +325,7 @@ User account Don't forget to allow login to the test user (`deployment-service-check`), otherwise the hub health check performed during deployment will fail. ``` -### Example config for CILogon +#### Example config for CILogon through Auth0 The CILogon connection works by providing users the option to login into a hub using any CILogon Identity Provider of their choice, as long as the email address of the user or the entire organization (e.g. `*@berkeley.edu`) has been provided access into the hub. From 6819a9e6a13fb9e3bbdf7fdda21809ad86cf9c68 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 22 Mar 2022 10:51:21 +0200 Subject: [PATCH 50/70] Fix authenticator naming --- docs/howto/configure/auth-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/configure/auth-management.md b/docs/howto/configure/auth-management.md index 784ccf6221..e2d9c6be86 100644 --- a/docs/howto/configure/auth-management.md +++ b/docs/howto/configure/auth-management.md @@ -241,7 +241,7 @@ The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar jupyterhub: hub: config: - GitHubOAuthenticator: + CILogonOAuthenticator: client_id: CLIENT_ID client_secret: CLIENT_SECRET ``` From 249e8a62aed3eadd5e97f569f750fae3848a26f9 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 22 Mar 2022 11:42:35 +0200 Subject: [PATCH 51/70] Move the cilogon auth scopes from being hub specific to the basehub helm chart --- config/clusters/2i2c/dask-staging.values.yaml | 4 ---- helm-charts/basehub/values.yaml | 6 ++++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/clusters/2i2c/dask-staging.values.yaml b/config/clusters/2i2c/dask-staging.values.yaml index f1e0c2e339..0f357ffbb8 100644 --- a/config/clusters/2i2c/dask-staging.values.yaml +++ b/config/clusters/2i2c/dask-staging.values.yaml @@ -35,10 +35,6 @@ basehub: authenticator_class: cilogon CILogonOAuthenticator: username_claim: "email" - scope: - - openid - - email - - org.cilogon.userinfo oauth_callback_url: "https://dask-staging.2i2c.cloud/hub/oauth_callback" allowed_idps: - "2i2c.org" diff --git a/helm-charts/basehub/values.yaml b/helm-charts/basehub/values.yaml index cd82dd699a..0d873ba323 100644 --- a/helm-charts/basehub/values.yaml +++ b/helm-charts/basehub/values.yaml @@ -423,3 +423,9 @@ jupyterhub: if get_config("custom.docs_service.enabled"): c.JupyterHub.services.append({"name": "docs", "url": "http://docs-service"}) + 09-add-cilogon-scopes-if-enabled: | + from z2jh import get_config + + authenticator_class = get_config("hub.config.JupyterHub.authenticator_class") + if authenticator_class == "cilogon": + c.CILogonOAuthenticator.scope = ["openid", "email", "org.cilogon.userinfo"] From d607b333a09c0fd61fd5bf535fad1dc9fcbdf303 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 22 Mar 2022 11:44:22 +0200 Subject: [PATCH 52/70] Add upstream issue as a ref --- docs/howto/configure/auth-management.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/howto/configure/auth-management.md b/docs/howto/configure/auth-management.md index e2d9c6be86..c2384bd04e 100644 --- a/docs/howto/configure/auth-management.md +++ b/docs/howto/configure/auth-management.md @@ -277,7 +277,8 @@ The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar When using this method of authentication, make sure to remove the `allowed_users` key from the config. This is because this key will block any user not listed under it **even if** they are valid members of the the organisation or team you are authenticating against. - Also, the `admin_users` list need to match `allowed_idps` currently. TODO: we should fix this upstream. + Also, the `admin_users` list need to match `allowed_idps` currently. + Reference https://github.com/jupyterhub/oauthenticator/issues/494. ``` To authenticate using CILogon, allowing only a certain identity provider: From 2c49f3cd4d81114b9b1ce47f2b6096f0c43633e3 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 22 Mar 2022 12:12:21 +0200 Subject: [PATCH 53/70] Remove _get, _post_, _puth methods --- deployer/cilogon_app.py | 41 ++++++++--------------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index a2db943207..4e706e44cb 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -38,36 +38,6 @@ def _url(self, id=None): return str(URL(url).with_query({"client_id": id})) - def _post(self, url, data=None): - headers = self.base_headers.copy() - - return requests.post(url, json=data, headers=headers, timeout=self.timeout) - - def _get(self, url, params=None): - headers = self.base_headers.copy() - - timeout = time.time() + 10 - t = 0.05 - - # Allow up to 10s of retrying getting the response - while time.time() < timeout: - response = requests.get( - url, params=params, headers=headers, timeout=self.timeout - ) - if response.status_code == 200: - return response - - t = min(2, t * 2) - time.sleep(t) - - # Return latest response to the request - return response - - def _put(self, url, data=None): - headers = self.base_headers.copy() - - return requests.put(url, json=data, headers=headers, timeout=self.timeout) - def create(self, body): """Creates a new client @@ -77,7 +47,9 @@ def create(self, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-post.sh """ - response = self._post(self._url(), data=body) + headers = self.base_headers.copy() + response = requests.post(self._url(), json=body, headers=headers, timeout=self.timeout) + client_name = body["client_name"] if response.status_code != 200: @@ -98,7 +70,8 @@ def get(self, id): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-get.sh """ - response = self._get(self._url(id)) + headers = self.base_headers.copy() + response = requests.get(self._url(id), params=None, headers=headers, timeout=self.timeout) if response.status_code != 200: print( @@ -120,7 +93,9 @@ def update(self, id, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-put.sh """ - response = self._put(self._url(id), data=body) + headers = self.base_headers.copy() + response = requests.put(self._url(id), json=body, headers=headers, timeout=self.timeout) + client_name = body["client_name"] if response.status_code != 200: From 1d7fbea038cf22e44c9b6867560d50e966e64580 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Mar 2022 10:13:14 +0000 Subject: [PATCH 54/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_app.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index 4e706e44cb..05d54029e5 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -48,7 +48,9 @@ def create(self, body): """ headers = self.base_headers.copy() - response = requests.post(self._url(), json=body, headers=headers, timeout=self.timeout) + response = requests.post( + self._url(), json=body, headers=headers, timeout=self.timeout + ) client_name = body["client_name"] @@ -71,7 +73,9 @@ def get(self, id): """ headers = self.base_headers.copy() - response = requests.get(self._url(id), params=None, headers=headers, timeout=self.timeout) + response = requests.get( + self._url(id), params=None, headers=headers, timeout=self.timeout + ) if response.status_code != 200: print( @@ -94,7 +98,9 @@ def update(self, id, body): See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-put.sh """ headers = self.base_headers.copy() - response = requests.put(self._url(id), json=body, headers=headers, timeout=self.timeout) + response = requests.put( + self._url(id), json=body, headers=headers, timeout=self.timeout + ) client_name = body["client_name"] From 04d4a8bd8d7150006d24cce3bf95617d6578a5b1 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 22 Mar 2022 14:07:54 +0200 Subject: [PATCH 55/70] Add the option to delete a client and to retrieve all clients --- deployer/cilogon_app.py | 105 ++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 14 deletions(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index 05d54029e5..53abc5701a 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -44,6 +44,8 @@ def create(self, body): Args: body (dict): Attributes for the new client + Returns a dict containing the client details. + See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-post.sh """ @@ -63,12 +65,14 @@ def create(self, body): print(f"Successfully created a new CILogon client for {client_name}!") return response.json() - def get(self, id): + def get(self, id=None): """Retrieves a client by its id. Args: id (str): Id of the client to get + Returns a dict containing the client details. + See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-get.sh """ @@ -95,6 +99,8 @@ def update(self, id, body): id (str): Id of the client to modify body (dict): Attributes to modify. + Returns 200 OK or raises an error if the update request returned anything other than 200.. + See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-put.sh """ headers = self.base_headers.copy() @@ -113,6 +119,29 @@ def update(self, id, body): print("Client updated succesfuly!") return response.status_code + def delete(self, id): + """Deletes the client associated with the id. + + Args: + id (str): Id of the client to delete + + Returns 200 OK or raises an error if the delete request returned anything other than 200. + + See: https://github.com/ncsa/OA4MP/blob/HEAD/oa4mp-server-admin-oauth2/src/main/scripts/oidc-cm-scripts/cm-delete.sh + """ + + headers = self.base_headers.copy() + response = requests.delete(self._url(id), headers=headers, timeout=self.timeout) + + if response.status_code != 200: + print( + f"An error occured when deleting the {id} client. \n Error was {response.text}." + ) + response.raise_for_status() + + print(f"Successfully deleted the {id} client!") + return response.status_code + class CILogonClientProvider: def __init__(self, admin_id, admin_secret): @@ -186,8 +215,9 @@ def _load_client_id(self, config_filename): ] except FileNotFoundError: print( - "The CILogon client you requested to update doesn't exist! Please create it first." + "Oops! The CILogon client you requested to doesn't exist! Please create it first." ) + return def create_client(self, cluster_name, hub_name, hub_type, callback_url): client_details = self._build_client_details( @@ -218,33 +248,49 @@ def update_client(self, cluster_name, hub_name, callback_url): ) config_filename = self._build_config_filename(cluster_name, hub_name) - if not Path(config_filename).is_file(): - print( - "Oops! No CILogon client has been found for this hub! Use the create subcommand to create one." - ) - return - client_id = self._load_client_id(config_filename) + # No client has been found for this hub + if not client_id: + return + print(f"Updating the existing CILogon client for {cluster_name}-{hub_name}...") return self.admin_client.update(client_id, client_details) def get_client(self, cluster_name, hub_name): config_filename = self._build_config_filename(cluster_name, hub_name) - if not Path(config_filename).is_file(): - print( - "Oops! No CILogon client has been found for this hub! Use the `create` command to create one." - ) - return - client_id = self._load_client_id(config_filename) + # No client has been found + if not client_id: + return + print( f"Getting the stored CILogon client details for {cluster_name}-{hub_name}..." ) print(self.admin_client.get(client_id)) + def delete_client(self, cluster_name, hub_name): + config_filename = self._build_config_filename(cluster_name, hub_name) + + client_id = self._load_client_id(config_filename) + + # No client has been found + if not client_id: + return + + print( + f"Deleting the CILogon client details for {cluster_name}-{hub_name}..." + ) + print(self.admin_client.delete(client_id)) + + def get_all_clients(self): + print( + f"Getting all existing OAauth client applications..." + ) + print(self.admin_client.get()) + def main(): argparser = argparse.ArgumentParser( @@ -332,6 +378,30 @@ def main(): help="The hub for which we'll retrieve the CILogon client details.", ) + # Get all subcommand + subparsers.add_parser( + "get-all", + help="Retrieve details about an existing CILogon client", + ) + + # Delete subcommand + delete_parser = subparsers.add_parser( + "delete", + help="Delete an existing CILogon client", + ) + + delete_parser.add_argument( + "cluster_name", + type=str, + help="The name of the cluster where the hub lives", + ) + + delete_parser.add_argument( + "hub_name", + type=str, + help="The hub for which we'll delete the CILogon client details.", + ) + args = argparser.parse_args() # This filepath is relative to the PROJECT ROOT @@ -362,6 +432,13 @@ def main(): args.cluster_name, args.hub_name, ) + elif args.action == "delete": + cilogon.delete_client( + args.cluster_name, + args.hub_name, + ) + elif args.action == "get-all": + cilogon.get_all_clients() if __name__ == "__main__": From 37d536a93b9e59989c0aad11a48fdb1bfb8f4463 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Mar 2022 12:08:17 +0000 Subject: [PATCH 56/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_app.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index 53abc5701a..f5ef4cc399 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -280,15 +280,11 @@ def delete_client(self, cluster_name, hub_name): if not client_id: return - print( - f"Deleting the CILogon client details for {cluster_name}-{hub_name}..." - ) + print(f"Deleting the CILogon client details for {cluster_name}-{hub_name}...") print(self.admin_client.delete(client_id)) def get_all_clients(self): - print( - f"Getting all existing OAauth client applications..." - ) + print(f"Getting all existing OAauth client applications...") print(self.admin_client.get()) From 3b4853998dcf8db2e22b76eaf2d474cdb129d16a Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 23 Mar 2022 11:54:00 +0200 Subject: [PATCH 57/70] Fix pre-commit --- deployer/cilogon_app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index f5ef4cc399..5bec2f1c0d 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -2,7 +2,6 @@ import base64 import requests import subprocess -import time from pathlib import Path from ruamel.yaml import YAML @@ -284,7 +283,9 @@ def delete_client(self, cluster_name, hub_name): print(self.admin_client.delete(client_id)) def get_all_clients(self): - print(f"Getting all existing OAauth client applications...") + print( + "Getting all existing OAauth client applications..." + ) print(self.admin_client.get()) From 3edbdd62b54998789a5f433e4baabf13f1025653 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 23 Mar 2022 13:37:35 +0200 Subject: [PATCH 58/70] Add short description about the script --- deployer/cilogon_app.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index 5bec2f1c0d..046edb29db 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -1,3 +1,28 @@ +""" +### Summary + +This is a helper script that can create/update/get/delete CILogon clients +using the 2i2c administrative client provided by CILogon. +More details here: https://cilogon.github.io/oa4mp/server/manuals/dynamic-client-registration.html + +### Use cases + +The script can be used to: + +- `create` a CILogon OAuth application for a hub and store the credentials safely +- `update` the callback urls of an existing hub CILogon client +- `delete` a CILogon OAuth application when a hub is removed or changes auth methods +- `get` details about an existing hub CILogon client +- `get-all` existing 2i2c CILogon OAuth applications + +### Running the script + +Get usage instructions about each of the available subcommands (create/update/get/get-all/delete) +by executing the script with `--help` flag, from the root of the repository: + +- python extra_scripts/cilogon_app.py create --help +""" + import argparse import base64 import requests From 40c194d3e022ffe34c482974c5c1bc63c3822138 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 23 Mar 2022 13:47:23 +0200 Subject: [PATCH 59/70] Add docs on how/why to remove a cilogon client --- docs/howto/configure/auth-management.md | 2 +- docs/howto/operate/delete-hub.md | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/howto/configure/auth-management.md b/docs/howto/configure/auth-management.md index c2384bd04e..85c53f0c41 100644 --- a/docs/howto/configure/auth-management.md +++ b/docs/howto/configure/auth-management.md @@ -226,7 +226,7 @@ User account The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar with the ones for enabling GitHubOAuthenticator: 1. **Create a CILogon OAuth client** - This can be achieved by using the [cilogon_app.py](https://github.com/2i2c-org/infrastructure/blob/master/deployer/cilogon_app.py) script. + This can be achieved by using the [cilogon_app.py](https://github.com/2i2c-org/infrastructure/blob/HEAD/deployer/cilogon_app.py) script. - The script needs to be passed the cluster and hub name for which a client id and secret will be generated, but also the hub type, and the authorisation callback URL. - The authorisation callback URL is the homepage url appended with `/hub/oauth_callback`. For example, `staging.pilot.2i2c.cloud/hub/oauth_callback`. diff --git a/docs/howto/operate/delete-hub.md b/docs/howto/operate/delete-hub.md index 29cbfdb232..ff9f9b738f 100644 --- a/docs/howto/operate/delete-hub.md +++ b/docs/howto/operate/delete-hub.md @@ -20,9 +20,18 @@ If you'd like to delete a hub, there are a few steps that we need to take: `kubectl delete namespace `, to cleanup any possible leftovers that step (3) missed -6. **Delete our Auth0 application**. For each hub, we create an +6. **Delete the OAuth application**. For each hub that uses Auth0, we create an [Application](https://auth0.com/docs/applications) in Auth0. There is a limited number of Auth0 applications available, so we should delete the one used by this hub when it's done. You should be able to see the [list of applications](https://manage.auth0.com/dashboard/us/2i2c/applications) if you login to auth0 with your 2i2c google account. + + Similarly, for each hub that uses CILogon, we dynamically create an OAuth + [client application](https://cilogon.github.io/oa4mp/server/manuals/dynamic-client-registration.html) + in CILogon using the [cilogon_app.py](https://github.com/2i2c-org/infrastructure/blob/HEAD/deployer/cilogon_app.py) + script. Use the script to delete this CILogon client when a hub is removed. + + ```bash + python deployer/cilogon_app.py delete + ``` From adeba0f1823f1e633767e711aa921f52df399788 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 23 Mar 2022 11:47:51 +0000 Subject: [PATCH 60/70] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deployer/cilogon_app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index 046edb29db..a2034a49ad 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -308,9 +308,7 @@ def delete_client(self, cluster_name, hub_name): print(self.admin_client.delete(client_id)) def get_all_clients(self): - print( - "Getting all existing OAauth client applications..." - ) + print("Getting all existing OAauth client applications...") print(self.admin_client.get()) From d51df2a5c96302e7cde0123a62fc841464cd3626 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 23 Mar 2022 15:05:15 +0200 Subject: [PATCH 61/70] Fix location --- deployer/cilogon_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index 046edb29db..d51f8ba5be 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -20,7 +20,7 @@ Get usage instructions about each of the available subcommands (create/update/get/get-all/delete) by executing the script with `--help` flag, from the root of the repository: -- python extra_scripts/cilogon_app.py create --help +- python deployer/cilogon_app.py create --help """ import argparse From e98cae3ab1666291893b3d90e740f52f31750f2e Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 23 Mar 2022 14:43:06 -0700 Subject: [PATCH 62/70] Move CILogonOAuthenticator default config to values.yaml We can put default values there even if they aren't enabled --- docs/howto/configure/auth-management.md | 8 ++------ helm-charts/basehub/values.yaml | 14 ++++++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/howto/configure/auth-management.md b/docs/howto/configure/auth-management.md index 85c53f0c41..1c345438bd 100644 --- a/docs/howto/configure/auth-management.md +++ b/docs/howto/configure/auth-management.md @@ -227,14 +227,14 @@ The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar 1. **Create a CILogon OAuth client** This can be achieved by using the [cilogon_app.py](https://github.com/2i2c-org/infrastructure/blob/HEAD/deployer/cilogon_app.py) script. - + - The script needs to be passed the cluster and hub name for which a client id and secret will be generated, but also the hub type, and the authorisation callback URL. - The authorisation callback URL is the homepage url appended with `/hub/oauth_callback`. For example, `staging.pilot.2i2c.cloud/hub/oauth_callback`. - Example script invocation that creates a CILogon OAuth client for the 2i2c dask-staging hub: ```bash python3 ./deployer/cilogon_auth.py create 2i2c dask-staging daskhub https://dask-staging.2i2c.cloud/hub/oauth_callback ``` - - If successfull, the script will have created a secret values file under `config/clusters//enc-.secret.values.yaml`. This file + - If successfull, the script will have created a secret values file under `config/clusters//enc-.secret.values.yaml`. This file holds the encrypted OAuth client id and secret that have been created for this hub. - The unecrypted file contents should look like this: ```yaml @@ -292,10 +292,6 @@ The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar CILogonOAuthenticator: oauth_callback_url: https://{{ HUB_DOMAIN }}/hub/oauth_callback username_claim: USERNAME_KEY - scope: - - openid - - email - - org.cilogon.userinfo allowed_idps: - 2i2c.org - IDP diff --git a/helm-charts/basehub/values.yaml b/helm-charts/basehub/values.yaml index 0d873ba323..2fb5e32738 100644 --- a/helm-charts/basehub/values.yaml +++ b/helm-charts/basehub/values.yaml @@ -244,6 +244,14 @@ jupyterhub: matchLabels: app.kubernetes.io/component: traefik hub: + config: + # Default options for CILogonOAuthenticator, for hubs where it is enabled + CILogonOAuthenticator: + scope: + - openid + - email + - org.cilogon.userinfo + extraFiles: configurator-schema-default: mountPath: /usr/local/etc/jupyterhub-configurator/00-default.schema.json @@ -423,9 +431,3 @@ jupyterhub: if get_config("custom.docs_service.enabled"): c.JupyterHub.services.append({"name": "docs", "url": "http://docs-service"}) - 09-add-cilogon-scopes-if-enabled: | - from z2jh import get_config - - authenticator_class = get_config("hub.config.JupyterHub.authenticator_class") - if authenticator_class == "cilogon": - c.CILogonOAuthenticator.scope = ["openid", "email", "org.cilogon.userinfo"] From 209489207e77d1363cad60818ab4822f482b121f Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 23 Mar 2022 14:43:35 -0700 Subject: [PATCH 63/70] Add warning about picking USERNAME_KEY --- docs/howto/configure/auth-management.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/howto/configure/auth-management.md b/docs/howto/configure/auth-management.md index 1c345438bd..f0ce998854 100644 --- a/docs/howto/configure/auth-management.md +++ b/docs/howto/configure/auth-management.md @@ -296,8 +296,20 @@ The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar - 2i2c.org - IDP ``` - - Check the [CILogon scopes section](https://www.cilogon.org/oidc#h.p_PEQXL8QUjsQm) to checkout available values for `USERNAME_KEY` claim. - - Per [CILogon's suggestion]((https://www.cilogon.org/oidc#h.p_PEQXL8QUjsQm)), please use the same list of scopes in this example when enabling CILogon for a hub. + + Check the [CILogon scopes + section](https://www.cilogon.org/oidc#h.p_PEQXL8QUjsQm) to checkout available + values for `USERNAME_KEY` claim. This *cannot* be changed afterwards without manual + migration of user names, so choose this carefully. + + ```{warning} + `USERNAME_KEY` should be something the user *cannot change* in any of the identity providers + we support. If they can, it can be easily used to impersonate others! For example, if we allow + both GitHub and `utoronto.ca` as allowed authentication providers, and only use `email` as + `USERNAME_KEY`, any GitHub user can set their email field in their GitHub profile to a `utoronto.ca` + email and thus gain access to any `utoronto.ca` user's server! So a very careful choice needs to + be made here. + ``` 6. Run the deployer as normal to apply the config. From 09925bfd5d33ae726396a069ef1607709762afe2 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 23 Mar 2022 14:47:32 -0700 Subject: [PATCH 64/70] Invoke python as python3 rather than just as python --- docs/howto/operate/delete-hub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/operate/delete-hub.md b/docs/howto/operate/delete-hub.md index ff9f9b738f..d7bbfa6dbe 100644 --- a/docs/howto/operate/delete-hub.md +++ b/docs/howto/operate/delete-hub.md @@ -33,5 +33,5 @@ If you'd like to delete a hub, there are a few steps that we need to take: script. Use the script to delete this CILogon client when a hub is removed. ```bash - python deployer/cilogon_app.py delete + python3 deployer/cilogon_app.py delete ``` From d18ed4f5087a56a97b3be5487b1b5cf8b25acc48 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 23 Mar 2022 15:37:06 -0700 Subject: [PATCH 65/70] Fix typo in name of cilogon script --- docs/howto/configure/auth-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/configure/auth-management.md b/docs/howto/configure/auth-management.md index f0ce998854..b79b156e45 100644 --- a/docs/howto/configure/auth-management.md +++ b/docs/howto/configure/auth-management.md @@ -232,7 +232,7 @@ The steps to enable the JupyterHub CILogonOAuthenticator for a hub are simmilar - The authorisation callback URL is the homepage url appended with `/hub/oauth_callback`. For example, `staging.pilot.2i2c.cloud/hub/oauth_callback`. - Example script invocation that creates a CILogon OAuth client for the 2i2c dask-staging hub: ```bash - python3 ./deployer/cilogon_auth.py create 2i2c dask-staging daskhub https://dask-staging.2i2c.cloud/hub/oauth_callback + python3 ./deployer/cilogon_app.py create 2i2c dask-staging daskhub https://dask-staging.2i2c.cloud/hub/oauth_callback ``` - If successfull, the script will have created a secret values file under `config/clusters//enc-.secret.values.yaml`. This file holds the encrypted OAuth client id and secret that have been created for this hub. From ca3631cc0f6738d396705aa3b0fd47ca2d1e9660 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 23 Mar 2022 15:46:50 -0700 Subject: [PATCH 66/70] Move demo hub to use cilogon directly --- config/clusters/2i2c/cluster.yaml | 5 ++--- config/clusters/2i2c/demo.values.yaml | 7 +++++++ .../clusters/2i2c/enc-demo.secret.values.yaml | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 config/clusters/2i2c/enc-demo.secret.values.yaml diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index 519bcad9bf..16b7fe9555 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -43,13 +43,12 @@ hubs: domain: demo.2i2c.cloud helm_chart: basehub auth0: - # connection update? Also ensure the basehub Helm chart is provided a - # matching value for jupyterhub.custom.2i2c.add_staff_user_ids_of_type! - connection: CILogon + enabled: false helm_chart_values_files: # The order in which you list files here is the order the will be passed # to the helm upgrade command in, and that has meaning. Please check # that you intend for these files to be applied in this order. + - enc-demo.secret.values.yaml - demo.values.yaml - name: ohw display_name: "Ocean Hack Week" diff --git a/config/clusters/2i2c/demo.values.yaml b/config/clusters/2i2c/demo.values.yaml index 2aac263e4e..4f6d75e0f9 100644 --- a/config/clusters/2i2c/demo.values.yaml +++ b/config/clusters/2i2c/demo.values.yaml @@ -20,7 +20,14 @@ jupyterhub: url: https://2i2c.org hub: config: + JupyterHub: + authenticator_class: cilogon Authenticator: # We do not define allowed_users here since only usernames matching this regex will be allowed to login into the hub. # Ref: https://jupyterhub.readthedocs.io/en/stable/api/auth.html#jupyterhub.auth.Authenticator.username_pattern username_pattern: '^(.+@2i2c\.org|.+@rmbl\.org|deployment-service-check)$' + CILogonAuthenticator: + oauth_callback_url: https://demo.2i2c.cloud/hub/oauth_callback + username_claim: email + allowed_idps: + - "2i2c.org" diff --git a/config/clusters/2i2c/enc-demo.secret.values.yaml b/config/clusters/2i2c/enc-demo.secret.values.yaml new file mode 100644 index 0000000000..6974597da6 --- /dev/null +++ b/config/clusters/2i2c/enc-demo.secret.values.yaml @@ -0,0 +1,20 @@ +jupyterhub: + hub: + config: + CILogonOAuthenticator: + client_id: ENC[AES256_GCM,data:I2OU6vxq+1MH1xjk5Zy4sVjFtMdgsOgG7lYa0UKJRAaG8csBnXIUi69yHJkkBojvGHBQ,iv:LV8iaQFVBr/iotKhFpilLIM1biVQlDLDJrZuV9IQDA8=,tag:ztZxAQZY+25qmUFfvN5w7A==,type:str] + client_secret: ENC[AES256_GCM,data:+fI/SCEsygTP6sQDcybSrpd4IO6KhpiKzNI59+C/mFRF4TNas7+zejRuzYVzJAKTjkEL1NIOek7DNLj47OcBL9Mm5iYyJM3f4gSImUo0QYLf0hnWMOA=,iv:GS2I3kv0IvOVDZht87oTT7LaWswXWwot95R2XWAbKBk=,tag:gI8fVcfLku52wYRHq9/kqQ==,type:str] +sops: + kms: [] + gcp_kms: + - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs + created_at: "2022-03-23T22:30:17Z" + enc: CiQA4OM7eIjffZKhqikREKUP2NJFoQ430IThTbFbNkrkPcPA++USSQDm5XgW0KL4+aPX+H0Xg0g9Y293y/8SEpifqE1T8On8Na0Phf6AMlg99x35lF1Sc9rTmnuBftzMaZ6YfsW3IVOwj+7fIbMuNWw= + azure_kv: [] + hc_vault: [] + age: [] + lastmodified: "2022-03-23T22:30:18Z" + mac: ENC[AES256_GCM,data:KipPDtFhkpA1DR+T2tEwqsRb2rAxg5woOLbl4tB75DWhQ4cSr2ne7EoHuNRs7GCJh0KZ19Sh5wu5ujpUQp3BlhUW/dj5h1W8f00e+T5RoroNU2VIWoG+C5Utgb84h6XLjz0IqPY9Z7TgWJcY6TTYJMF59DxUdyjhfWM3oSUGf2I=,iv:eGdldOja6bMbqrLEh/i6aaE20KOJ/Im626Ht22BOtT8=,tag:82paRer+fGTIqrJdjJEOWg==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.1 From f23cce45ac97818158d86ecd1699e62b0772915a Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Thu, 24 Mar 2022 15:09:32 -0700 Subject: [PATCH 67/70] Fix typo Co-authored-by: Georgiana Elena --- config/clusters/2i2c/demo.values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/clusters/2i2c/demo.values.yaml b/config/clusters/2i2c/demo.values.yaml index 4f6d75e0f9..a7d3a2071f 100644 --- a/config/clusters/2i2c/demo.values.yaml +++ b/config/clusters/2i2c/demo.values.yaml @@ -26,7 +26,7 @@ jupyterhub: # We do not define allowed_users here since only usernames matching this regex will be allowed to login into the hub. # Ref: https://jupyterhub.readthedocs.io/en/stable/api/auth.html#jupyterhub.auth.Authenticator.username_pattern username_pattern: '^(.+@2i2c\.org|.+@rmbl\.org|deployment-service-check)$' - CILogonAuthenticator: + CILogonOAuthenticator: oauth_callback_url: https://demo.2i2c.cloud/hub/oauth_callback username_claim: email allowed_idps: From 7305f6377766ae4bf3e5db17a854d0cc4d66633e Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 24 Mar 2022 15:30:40 -0700 Subject: [PATCH 68/70] Remove allowed_idps for demo hub Continue to rely on username_pattern right now as we figure out how to use IDPs --- config/clusters/2i2c/demo.values.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/clusters/2i2c/demo.values.yaml b/config/clusters/2i2c/demo.values.yaml index a7d3a2071f..9bd1c65dfd 100644 --- a/config/clusters/2i2c/demo.values.yaml +++ b/config/clusters/2i2c/demo.values.yaml @@ -29,5 +29,3 @@ jupyterhub: CILogonOAuthenticator: oauth_callback_url: https://demo.2i2c.cloud/hub/oauth_callback username_claim: email - allowed_idps: - - "2i2c.org" From aadf12fbd761056e01c74eb2f33f762a7b8ef806 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 24 Mar 2022 16:28:45 -0700 Subject: [PATCH 69/70] Remove CILogonOAuthenticator default scopes These are already actually set in the base CILogonOAuthenticator itself! https://github.com/jupyterhub/oauthenticator/blob/8c1cf0c616d5e19d1f93238673f17db2daf57fa0/oauthenticator/cilogon.py#L69 --- helm-charts/basehub/values.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/helm-charts/basehub/values.yaml b/helm-charts/basehub/values.yaml index 2fb5e32738..cd82dd699a 100644 --- a/helm-charts/basehub/values.yaml +++ b/helm-charts/basehub/values.yaml @@ -244,14 +244,6 @@ jupyterhub: matchLabels: app.kubernetes.io/component: traefik hub: - config: - # Default options for CILogonOAuthenticator, for hubs where it is enabled - CILogonOAuthenticator: - scope: - - openid - - email - - org.cilogon.userinfo - extraFiles: configurator-schema-default: mountPath: /usr/local/etc/jupyterhub-configurator/00-default.schema.json From 939a91c9229138b21a26c876121836156f60aa64 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Fri, 25 Mar 2022 14:07:27 +0200 Subject: [PATCH 70/70] Print clint list preetier --- deployer/cilogon_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deployer/cilogon_app.py b/deployer/cilogon_app.py index 6f1ee43a85..342dfde7bf 100644 --- a/deployer/cilogon_app.py +++ b/deployer/cilogon_app.py @@ -309,7 +309,9 @@ def delete_client(self, cluster_name, hub_name): def get_all_clients(self): print("Getting all existing OAauth client applications...") - print(self.admin_client.get()) + clients = self.admin_client.get() + for c in clients["clients"]: + print(c) def main():