From 2ee959cd2e3fede8ce61c6a75d505e60ba9831df Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 26 Sep 2023 02:56:02 -0700 Subject: [PATCH] feat: add custom oauth support (#2336) * custom oauth * all * revert ee changes * fix frontend --- backend/windmill-api/src/oauth2.rs | 131 +++++++++------ frontend/src/lib/components/AppConnect.svelte | 152 +++++++----------- .../src/lib/components/CustomOauth.svelte | 76 +++++++++ .../lib/components/InstanceSettings.svelte | 61 +++++-- .../src/lib/components/KeycloakSetting.svelte | 2 +- .../src/lib/components/OAuthSetting.svelte | 4 +- .../lib/components/OauthExtraParams.svelte | 50 ++++++ .../src/lib/components/OauthScopes.svelte | 41 +++++ .../src/lib/components/OktaSetting.svelte | 2 +- frontend/tailwind.config.cjs | 3 - 10 files changed, 362 insertions(+), 160 deletions(-) create mode 100644 frontend/src/lib/components/CustomOauth.svelte create mode 100644 frontend/src/lib/components/OauthExtraParams.svelte create mode 100644 frontend/src/lib/components/OauthScopes.svelte diff --git a/backend/windmill-api/src/oauth2.rs b/backend/windmill-api/src/oauth2.rs index d81d50e88d..fe06b35450 100644 --- a/backend/windmill-api/src/oauth2.rs +++ b/backend/windmill-api/src/oauth2.rs @@ -182,7 +182,7 @@ pub fn build_oauth_clients( .as_ref() .map(|c| (x.0.clone(), (x.1, c.clone()))) })) - .map(|(k, (client_params, config))| { + .filter_map(|(k, (client_params, config))| { let named_client = build_basic_client( k.clone(), config.clone(), @@ -191,17 +191,25 @@ pub fn build_oauth_clients( base_url, None, ); - ( - named_client.0, - ClientWithScopes { - client: named_client.1, - scopes: config.scopes.unwrap_or(vec![]), - extra_params: config.extra_params, - extra_params_callback: config.extra_params_callback, - allowed_domains: client_params.allowed_domains.clone(), - userinfo_url: config.userinfo_url, - }, - ) + named_client + .map(|named_client| { + ( + named_client.0, + ClientWithScopes { + client: named_client.1, + scopes: config.scopes.unwrap_or(vec![]), + extra_params: config.extra_params, + extra_params_callback: config.extra_params_callback, + allowed_domains: client_params.allowed_domains.clone(), + userinfo_url: config.userinfo_url, + }, + ) + }) + .map_err(|e| { + tracing::error!("Error building oauth client {k}: {e}"); + e + }) + .ok() }) .collect(); @@ -213,7 +221,7 @@ pub fn build_oauth_clients( .as_ref() .map(|c| (x.0.clone(), (x.1, c.clone()))) })) - .map(|(k, (client_params, config))| { + .filter_map(|(k, (client_params, config))| { let named_client = build_basic_client( k.clone(), config.clone(), @@ -226,43 +234,61 @@ pub fn build_oauth_clients( None }, ); - ( - named_client.0, - ClientWithScopes { - client: named_client.1, - scopes: config.scopes.unwrap_or(vec![]), - extra_params: config.extra_params, - extra_params_callback: config.extra_params_callback, - allowed_domains: None, - userinfo_url: None, - }, - ) + named_client + .map(|named_client| { + ( + named_client.0, + ClientWithScopes { + client: named_client.1, + scopes: config.scopes.unwrap_or(vec![]), + extra_params: config.extra_params, + extra_params_callback: config.extra_params_callback, + allowed_domains: None, + userinfo_url: None, + }, + ) + }) + .map_err(|e| { + tracing::error!("Error building oauth client {k}: {e}"); + e + }) + .ok() }) .collect(); - let slack = oauths.get("slack").map(|v| { - build_basic_client( - "slack".to_string(), - OAuthConfig { - auth_url: "https://slack.com/oauth/authorize".to_string(), - token_url: "https://slack.com/api/oauth.access".to_string(), - userinfo_url: None, - scopes: None, - extra_params: None, - extra_params_callback: None, - req_body_auth: None, - }, - v.clone(), - false, - base_url, - Some(format!("{base_url}/oauth/callback_slack")), - ) - .1 - }); - - Ok(AllClients { logins, connects, slack }) + let slack = oauths + .get("slack") + .map(|v| { + build_basic_client( + "slack".to_string(), + OAuthConfig { + auth_url: "https://slack.com/oauth/authorize".to_string(), + token_url: "https://slack.com/api/oauth.access".to_string(), + userinfo_url: None, + scopes: None, + extra_params: None, + extra_params_callback: None, + req_body_auth: None, + }, + v.clone(), + false, + base_url, + Some(format!("{base_url}/oauth/callback_slack")), + ) + .map(|x| x.1) + .map_err(|e| { + tracing::error!("Error building oauth slack client: {e}"); + e + }) + .ok() + }) + .flatten(); + let all_clients = AllClients { logins, connects, slack }; + tracing::info!("Final oauth config: {all_clients:#?}"); + Ok(all_clients) } +use anyhow::anyhow; pub fn build_basic_client( name: String, config: OAuthConfig, @@ -270,9 +296,11 @@ pub fn build_basic_client( login: bool, base_url: &str, override_callback: Option, -) -> (String, OClient) { - let auth_url = Url::parse(&config.auth_url).expect("Invalid authorization endpoint URL"); - let token_url = Url::parse(&config.token_url).expect("Invalid token endpoint URL"); +) -> error::Result<(String, OClient)> { + let auth_url = Url::parse(&config.auth_url) + .map_err(|e| anyhow!("Invalid authorization endpoint URL: {e}"))?; + let token_url = + Url::parse(&config.token_url).map_err(|e| anyhow!("Invalid token endpoint URL: {e}"))?; let redirect_url = if login { format!("{base_url}/user/login_callback/{name}") @@ -287,9 +315,12 @@ pub fn build_basic_client( client.set_auth_type(AuthType::RequestBody); } client.set_client_secret(client_params.secret.clone()); - client.set_redirect_url(Url::parse(&redirect_url).expect("Invalid redirect URL")); + client.set_redirect_url( + Url::parse(&redirect_url).map_err(|e| anyhow!("Invalid redirect URL: {e}"))?, + ); + // Set up the config for the Github OAuth2 process. - (name.to_string(), client) + Ok((name.to_string(), client)) } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/frontend/src/lib/components/AppConnect.svelte b/frontend/src/lib/components/AppConnect.svelte index 9e74269f47..c36a17fa3e 100644 --- a/frontend/src/lib/components/AppConnect.svelte +++ b/frontend/src/lib/components/AppConnect.svelte @@ -57,12 +57,10 @@ + + + + + + + diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index c63e13cb0e..5d744abcfe 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -14,6 +14,9 @@ import KeycloakSetting from './KeycloakSetting.svelte' import Alert from './common/alert/Alert.svelte' import { isCloudHosted } from '$lib/cloud' + import { capitalize } from '$lib/utils' + import { enterpriseLicense } from '$lib/stores' + import CustomOauth from './CustomOauth.svelte' export const settings: Record = { Core: [ @@ -125,7 +128,7 @@ } } initialOauths = (await SettingService.getGlobal({ key: 'oauths' })) ?? {} - oauths = { ...initialOauths } + oauths = JSON.parse(JSON.stringify(initialOauths)) initialValues = Object.fromEntries( ( await Promise.all( @@ -136,7 +139,7 @@ ) ).flat() ) - values = { ...initialValues } + values = JSON.parse(JSON.stringify(initialValues)) if (values['retention_period_secs'] == undefined) { values['retention_period_secs'] = 60 * 60 * 24 * 60 } @@ -158,7 +161,7 @@ name: 'server', requestBody: newServerConfig }) - serverConfig = { ...newServerConfig } + serverConfig = JSON.parse(JSON.stringify(newServerConfig)) } await Promise.all( allSettings @@ -174,7 +177,7 @@ await SettingService.setGlobal({ key: x.key, requestBody: { value: values?.[x.key] } }) }) ) - initialValues = { ...values } + initialValues = JSON.parse(JSON.stringify(initialValues)) if (!deepEqual(initialOauths, oauths)) { await SettingService.setGlobal({ @@ -183,7 +186,7 @@ value: oauths } }) - initialOauths = { ...oauths } + initialOauths = JSON.parse(JSON.stringify(oauths)) } } else { console.error('Values not loaded') @@ -208,6 +211,23 @@ } let to: string = '' + + const windmillBuiltins = [ + 'github', + 'gitlab', + 'bitbucket', + 'slack', + 'gsheets', + 'gdrive', + 'gmail', + 'gcal', + 'gcloud', + 'gworkspace', + 'basecamp', + 'linkedin' + ] + + let oauth_name = 'custom'
@@ -344,6 +364,11 @@ Without EE, the number of SSO users is limited to 50. SCIM/SAML is available on EE +
+ + The recommended workflow is to to save your oauth setting and test them directly on the + login or resource page +
@@ -354,6 +379,11 @@

OAuth

+ + After setting an oauth client, make sure that there is a corresponding resource type + with the same name with a "token" field in the admins workspace. + +
@@ -362,7 +392,7 @@ {#if oauths[k]}
- + { delete oauths[k] @@ -383,6 +413,9 @@ bind:value={oauths[k]['secret']} /> + {#if !windmillBuiltins.includes(k) && k != 'slack'} + + {/if}
{/if} @@ -390,20 +423,28 @@ {/each}
- + +
diff --git a/frontend/src/lib/components/KeycloakSetting.svelte b/frontend/src/lib/components/KeycloakSetting.svelte index d302c17f6f..9cb33a5c74 100644 --- a/frontend/src/lib/components/KeycloakSetting.svelte +++ b/frontend/src/lib/components/KeycloakSetting.svelte @@ -31,7 +31,7 @@
-
- diff --git a/frontend/src/lib/components/OauthExtraParams.svelte b/frontend/src/lib/components/OauthExtraParams.svelte new file mode 100644 index 0000000000..1e30fa2bf9 --- /dev/null +++ b/frontend/src/lib/components/OauthExtraParams.svelte @@ -0,0 +1,50 @@ + + +{#each extra_params_vec as o} +
+ + + +
+{/each} +
+ + + ({(extra_params_vec ?? []).length} item{(extra_params_vec ?? []).length > 1 ? 's' : ''}) + +
diff --git a/frontend/src/lib/components/OauthScopes.svelte b/frontend/src/lib/components/OauthScopes.svelte new file mode 100644 index 0000000000..8d73868a72 --- /dev/null +++ b/frontend/src/lib/components/OauthScopes.svelte @@ -0,0 +1,41 @@ + + +{#each scopes as v} +
+ + +
+{/each} +
+ + + ({(scopes ?? []).length} item{(scopes ?? []).length > 1 ? 's' : ''}) + +
diff --git a/frontend/src/lib/components/OktaSetting.svelte b/frontend/src/lib/components/OktaSetting.svelte index 99348d2940..d75316b1d3 100644 --- a/frontend/src/lib/components/OktaSetting.svelte +++ b/frontend/src/lib/components/OktaSetting.svelte @@ -33,7 +33,7 @@
-