From 670e55b34bb04f650e9a59896f3c74cab928df28 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 13 Jul 2022 10:35:08 +0200 Subject: [PATCH] feat: account part II, handle refresh tokens, clarify oauth UI (#196) --- ...20707094935_add_client_to_account.down.sql | 1 + ...0220707094935_add_client_to_account.up.sql | 25 +++ backend/oauth_connect.json | 14 ++ backend/openapi.yaml | 108 ++++++++++- backend/sqlx-data.json | 180 ++++++++++++++---- backend/src/oauth2.rs | 166 ++++++++++++++-- backend/src/resources.rs | 10 +- backend/src/users.rs | 2 +- backend/src/variables.rs | 42 +++- frontend/src/lib/components/AppConnect.svelte | 35 +++- frontend/src/lib/stores.ts | 3 +- .../oauth/callback/[client_name].svelte | 2 +- frontend/src/routes/resources.svelte | 24 ++- frontend/src/routes/runs/[...path].svelte | 2 +- frontend/src/routes/variables.svelte | 43 ++++- 15 files changed, 565 insertions(+), 92 deletions(-) create mode 100644 backend/migrations/20220707094935_add_client_to_account.down.sql create mode 100644 backend/migrations/20220707094935_add_client_to_account.up.sql diff --git a/backend/migrations/20220707094935_add_client_to_account.down.sql b/backend/migrations/20220707094935_add_client_to_account.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20220707094935_add_client_to_account.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20220707094935_add_client_to_account.up.sql b/backend/migrations/20220707094935_add_client_to_account.up.sql new file mode 100644 index 0000000000..149236f38e --- /dev/null +++ b/backend/migrations/20220707094935_add_client_to_account.up.sql @@ -0,0 +1,25 @@ +-- Add up migration script here +ALTER TABLE account ADD COLUMN owner VARCHAR(50) NOT NULL; +ALTER TABLE account ADD COLUMN client VARCHAR(50) NOT NULL; +ALTER TABLE resource ADD COLUMN is_oauth BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE variable ADD COLUMN is_oauth BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE resource DROP COLUMN account; + +ALTER TABLE account ALTER COLUMN expires_at TYPE TIMESTAMP WITH TIME ZONE; + +ALTER TABLE account ALTER COLUMN expires_at SET NOT NULL; +ALTER TABLE account ALTER COLUMN refresh_token SET NOT NULL; + +GRANT ALL ON account TO app; +GRANT ALL ON account TO admin; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO admin; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app; + +ALTER TABLE account ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY see_own ON account FOR ALL +USING (SPLIT_PART(account.owner, '/', 1) = 'u' AND SPLIT_PART(account.owner, '/', 2) = current_setting('session.user')); + +CREATE POLICY see_member ON account FOR ALL +USING (SPLIT_PART(account.owner, '/', 1) = 'g' AND SPLIT_PART(account.owner, '/', 2) = any(regexp_split_to_array(current_setting('session.groups'), ',')::text[])); diff --git a/backend/oauth_connect.json b/backend/oauth_connect.json index 2f465c7b0f..b34f015a2b 100644 --- a/backend/oauth_connect.json +++ b/backend/oauth_connect.json @@ -13,5 +13,19 @@ "scopes": [ "api" ] + }, + "bitbucket": { + "auth_url": "https://bitbucket.org/site/oauth2/authorize", + "token_url": "https://bitbucket.org/site/oauth2/access_token", + "scopes": [ + "repository" + ] + }, + "slack": { + "auth_url": "https://slack.com/oauth/authorize", + "token_url": "https://slack.com/api/oauth.access", + "scopes": [ + "chat:write:user" + ] } } diff --git a/backend/openapi.yaml b/backend/openapi.yaml index 50e335f3b2..adead1eea8 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -1067,12 +1067,75 @@ paths: content: application/json: schema: - type: object - properties: - token: - type: string + $ref: "#/components/schemas/TokenResponse" - /w/{workspace}/oauth/disconnect/{account_id}: + /w/{workspace}/oauth/create_account: + post: + summary: create OAuth account + operationId: createAccount + tags: + - oauth + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: code endpoint + required: true + content: + application/json: + schema: + type: object + properties: + refresh_token: + type: string + expires_in: + type: integer + owner: + type: string + client: + type: string + required: + - refresh_token + - expires_in + - owner + - client + responses: + "200": + description: account set + content: + text/plain: + schema: + type: string + + /w/{workspace}/oauth/refresh_token/{id}: + post: + summary: refresh token + operationId: refreshToken + tags: + - oauth + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/AccountId" + requestBody: + description: variable path + required: true + content: + application/json: + schema: + type: object + properties: + path: + type: string + required: + - path + responses: + "200": + description: token refreshed + content: + text/plain: + schema: + type: string + + /w/{workspace}/oauth/disconnect/{id}: post: summary: disconnect account operationId: disconnectAccount @@ -2690,7 +2753,7 @@ components: schema: type: string AccountId: - name: account + name: id in: path required: true schema: @@ -3187,6 +3250,10 @@ components: type: boolean description: type: string + account: + type: string + is_oauth: + type: boolean extra_perms: type: object additionalProperties: @@ -3222,6 +3289,10 @@ components: type: boolean description: type: string + account: + type: integer + is_oauth: + type: boolean required: - path - value @@ -3352,6 +3423,8 @@ components: type: string resource_type: type: string + is_oauth: + type: boolean required: - path - value @@ -3380,6 +3453,8 @@ components: type: string value: type: object + is_oauth: + type: boolean extra_perms: type: object additionalProperties: @@ -3387,6 +3462,7 @@ components: required: - path - resource_type + - is_oauth ResourceType: type: object @@ -3787,3 +3863,23 @@ components: - team_id - team_name - bot + + TokenResponse: + type: object + properties: + access_token: + type: string + expires_in: + type: integer + refresh_token: + type: string + scope: + type: array + items: + type: string + + required: + - access_token + - expires_in + - refresh_token + - scope diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 712fa927e4..22b85276bc 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -555,6 +555,23 @@ ] } }, + "27eb5f99dc9289670673fb999ba9e67abccba212b506c30ba47e6c4fef6d53c4": { + "query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Jsonb", + "Text", + "Varchar", + "Bool" + ] + }, + "nullable": [] + } + }, "28c042adef65c3055edc324fbbd2f267285d3566cbec58404983323d410ace27": { "query": "SELECT super_admin FROM password WHERE email = $1", "describe": { @@ -646,6 +663,24 @@ ] } }, + "2e4115bb2e6c8c85ad1492ad135d6b0454b342126cb5fa17e58caf71b32ee755": { + "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description, account, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6, $7)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Bool", + "Varchar", + "Int4", + "Bool" + ] + }, + "nullable": [] + } + }, "37d3ee8009055e869941e548a6d5a352053a5d7782f662c34b94706488abccb6": { "query": "UPDATE queue SET running = false WHERE last_ping < $1 RETURNING id", "describe": { @@ -1697,22 +1732,6 @@ ] } }, - "8a80333c2fbf7b50fed305882de6e4ffda985d5c648cd617add6c9e6a9c03f34": { - "query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Jsonb", - "Text", - "Varchar" - ] - }, - "nullable": [] - } - }, "8ad6a17eecce77f61236e0585ba89b99a32e07ad37b97662007db35acb59d139": { "query": "SELECT * from resource_type WHERE (workspace_id = $1 OR workspace_id = 'starter') ORDER BY name", "describe": { @@ -1937,8 +1956,8 @@ }, { "ordinal": 6, - "name": "account", - "type_info": "Int4" + "name": "is_oauth", + "type_info": "Bool" } ], "parameters": { @@ -1954,7 +1973,7 @@ true, false, false, - true + false ] } }, @@ -2278,22 +2297,6 @@ "nullable": [] } }, - "afe9ea97d6e4c45453e21dc3d218fe8927f9faa58921666ceb7014e1f695d900": { - "query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, $2, $3, $4, $5)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Varchar", - "Bool", - "Varchar" - ] - }, - "nullable": [] - } - }, "b20977e70ebac7ccbaec5a2a1e940301dd331a5f9a4be67a27cfbff8619ac8f0": { "query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, true)", "describe": { @@ -2395,6 +2398,27 @@ ] } }, + "bb56e61c7cfb09c0a28fb3226dfe91704c70d9fe15eda18e6889adfd7496f80b": { + "query": "DELETE FROM account WHERE workspace_id = $1 AND id = $2 RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, "be33c6eb702c149044650d49b3c50493d7538d590be3f4ff6242fea85c57c667": { "query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13)", "describe": { @@ -2598,6 +2622,21 @@ ] } }, + "cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748": { + "query": "UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Timestamptz", + "Text", + "Int4" + ] + }, + "nullable": [] + } + }, "cb12aee5f8e04cb196d4b8fad81699fcbb1ae7b0c84090d5705b14eac76074ff": { "query": "INSERT INTO group_\n VALUES ($1, 'all', 'The group that always contains all users of this workspace')", "describe": { @@ -2610,6 +2649,30 @@ "nullable": [] } }, + "d1513cfa13037cbfe076d184cc698b896add2daba02b8add600f643249b95ad2": { + "query": "INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, $4, $5) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Varchar" + ] + }, + "nullable": [ + false + ] + } + }, "d2dcf69b20488d610599c309862722f805049e479035be6a416d05d73528a8e1": { "query": "INSERT INTO group_\n (workspace_id, name, summary)\n VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", "describe": { @@ -3076,8 +3139,8 @@ }, { "ordinal": 6, - "name": "account", - "type_info": "Int4" + "name": "is_oauth", + "type_info": "Bool" } ], "parameters": { @@ -3092,8 +3155,49 @@ true, false, false, - true + false ] } + }, + "fbe14569717b4c4937b74e6dc6f6b4ea29a2b0c2b1cde48ef06d687f6f9e8f15": { + "query": "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "refresh_token", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false + ] + } + }, + "fcb6ce4fab662602827281e3e85fffaa48542f8d590100d0e7eead2ad4ad4873": { + "query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text" + ] + }, + "nullable": [] + } } } \ No newline at end of file diff --git a/backend/src/oauth2.rs b/backend/src/oauth2.rs index 01e93fb797..73a625fbce 100644 --- a/backend/src/oauth2.rs +++ b/backend/src/oauth2.rs @@ -8,6 +8,7 @@ use axum::extract::{Extension, FromRequest, Path, Query, RequestParts}; use axum::response::Redirect; use axum::routing::{get, post}; use axum::{async_trait, Json, Router}; +use chrono::{Duration, Utc}; use hyper::StatusCode; use itertools::Itertools; @@ -16,16 +17,18 @@ use reqwest::Client; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use slack_http_verifier::SlackVerifier; +use sqlx::{Postgres, Transaction}; use tokio::fs::File; use tokio::io::AsyncReadExt; use tower_cookies::{Cookie, Cookies}; use crate::audit::{audit_log, ActionKind}; use crate::db::{UserDB, DB}; -use crate::error::{self, to_anyhow, Result}; +use crate::error::{self, to_anyhow, Error, Result}; use crate::jobs; use crate::jobs::{get_latest_hash_for_path, JobPayload}; use crate::users::Authed; +use crate::utils::not_found_if_none; use crate::workspaces::WorkspaceSettings; use crate::BaseUrl; @@ -47,9 +50,12 @@ pub fn global_service() -> Router { pub fn workspaced_service() -> Router { Router::new() - .route("/disconnect/:account_id", post(disconnect)) + .route("/disconnect/:id", post(disconnect)) .route("/disconnect_slack", post(disconnect_slack)) .route("/set_workspace_slack", post(set_workspace_slack)) + .route("/create_account", post(create_account)) + .route("/delete_account/:id", post(delete_account)) + .route("/refresh_token/:id", post(refresh_token)) } pub struct ClientWithScopes { @@ -209,7 +215,7 @@ pub struct SlackTokenResponse { bot: SlackBotToken, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct TokenResponse { access_token: AccessToken, expires_in: Option, @@ -244,6 +250,68 @@ async fn connect( ) } +#[derive(Deserialize)] +struct CreateAccount { + client: String, + owner: String, + refresh_token: String, + expires_in: i64, +} +async fn create_account( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(payload): Json, +) -> error::Result { + let mut tx = user_db.begin(&authed).await?; + + let expires_at = chrono::Utc::now() + Duration::seconds(payload.expires_in); + let id = sqlx::query_scalar!( + "INSERT INTO account (workspace_id, client, owner, expires_at, refresh_token) VALUES ($1, $2, $3, $4, $5) RETURNING id", + w_id, + payload.client, + payload.owner, + expires_at, + payload.refresh_token + ) + .fetch_one(&mut tx) + .await?; + tx.commit().await?; + Ok(id.to_string()) +} + +async fn delete_account( + authed: Authed, + Extension(user_db): Extension, + Query((w_id, id)): Query<(String, i32)>, +) -> error::Result { + let mut tx = user_db.begin(&authed).await?; + + let exists = sqlx::query!( + "DELETE FROM account WHERE workspace_id = $1 AND id = $2 RETURNING id", + w_id, + id, + ) + .fetch_optional(&mut tx) + .await?; + + let id_str = id.to_string(); + not_found_if_none(exists, "Account", &id_str)?; + + audit_log( + &mut tx, + &authed.username, + "account.delete", + ActionKind::Delete, + &w_id, + Some(&id_str), + None, + ) + .await?; + tx.commit().await?; + Ok(format!("Deleted account id {id}")) +} + async fn list_logins( Extension(clients): Extension>, ) -> error::JsonResult> { @@ -333,24 +401,96 @@ async fn login( oauth_redirect(clients, client_name, cookies, None) } +#[derive(Deserialize)] +struct VariablePath { + path: String, +} +async fn refresh_token( + authed: Authed, + Path((w_id, id)): Path<(String, i32)>, + Json(VariablePath { path }): Json, + Extension(user_db): Extension, + Extension(clients): Extension>, + Extension(http_client): Extension, +) -> error::Result { + let tx = user_db.begin(&authed).await?; + + _refresh_token(tx, &path, w_id, id, clients, http_client).await?; + + Ok(format!("Token at path {path} refreshed")) +} + +pub async fn _refresh_token<'c>( + mut tx: Transaction<'c, Postgres>, + path: &str, + w_id: String, + id: i32, + clients: Arc, + http_client: Client, +) -> error::Result { + let account = sqlx::query!( + "SELECT client, refresh_token FROM account WHERE workspace_id = $1 AND id = $2", + w_id, + id, + ) + .fetch_optional(&mut tx) + .await?; + let account = not_found_if_none(account, "Account", &id.to_string())?; + let client = (&clients + .connects + .get(&account.client) + .ok_or_else(|| error::Error::BadRequest("invalid client".to_string()))? + .client) + .to_owned(); + let token = client + .exchange_refresh_token(&RefreshToken::from(account.refresh_token)) + .with_client(&http_client) + .execute::() + .await + .map_err(to_anyhow)?; + let expires_at = Utc::now() + + chrono::Duration::seconds( + token + .expires_in + .ok_or_else(|| Error::InternalErr("expires_in exepcted and not found".to_string()))? + .try_into() + .unwrap(), + ); + sqlx::query!( + "UPDATE account SET refresh_token = $1, expires_at = $2 WHERE workspace_id = $3 AND id = $4", + token.refresh_token.map(|x| x.to_string()), + expires_at, + w_id, + id + ) + .execute(&mut tx) + .await?; + let token = token.access_token.to_string(); + sqlx::query!( + "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3", + token, + w_id, + path + ) + .execute(&mut tx) + .await?; + tx.commit().await?; + Ok(token) +} + #[derive(Deserialize)] pub struct OAuthCallback { code: String, state: String, } -#[derive(Serialize)] -pub struct ConnectResponse { - token: String, -} - async fn connect_callback( cookies: Cookies, Path(client_name): Path, Json(callback): Json, Extension(clients): Extension>, Extension(http_client): Extension, -) -> error::JsonResult { +) -> error::JsonResult { let client = (&clients .connects .get(&client_name) @@ -358,12 +498,10 @@ async fn connect_callback( .client) .to_owned(); - let token = exchange_code::(callback, &cookies, client, &http_client) - .await? - .access_token - .to_string(); + let token_response = + exchange_code::(callback, &cookies, client, &http_client).await?; - Ok(Json(ConnectResponse { token })) + Ok(Json(token_response)) } async fn connect_slack_callback( diff --git a/backend/src/resources.rs b/backend/src/resources.rs index 7f30a3c0f9..61151c5fad 100644 --- a/backend/src/resources.rs +++ b/backend/src/resources.rs @@ -67,7 +67,7 @@ pub struct Resource { pub description: Option, pub resource_type: String, pub extra_perms: serde_json::Value, - pub account: Option, + pub is_oauth: bool, } #[derive(Deserialize)] @@ -76,6 +76,7 @@ pub struct CreateResource { pub value: Option, pub description: Option, pub resource_type: String, + pub is_oauth: Option, } #[derive(Deserialize)] struct EditResource { @@ -105,7 +106,7 @@ async fn list_resources( "description", "resource_type", "extra_perms", - "account", + "is_oauth", ]) .order_by("path", true) .and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id)) @@ -180,13 +181,14 @@ async fn create_resource( sqlx::query!( "INSERT INTO resource - (workspace_id, path, value, description, resource_type) - VALUES ($1, $2, $3, $4, $5)", + (workspace_id, path, value, description, resource_type, is_oauth) + VALUES ($1, $2, $3, $4, $5, $6)", w_id, resource.path, resource.value, resource.description, resource.resource_type, + resource.is_oauth ) .execute(&mut tx) .await?; diff --git a/backend/src/users.rs b/backend/src/users.rs index 3c06e7584a..f0027fe711 100644 --- a/backend/src/users.rs +++ b/backend/src/users.rs @@ -698,7 +698,7 @@ async fn decline_invite( &mut tx, &email, "users.decline_invite", - ActionKind::Create, + ActionKind::Delete, &nu.workspace_id, Some(&email), None, diff --git a/backend/src/variables.rs b/backend/src/variables.rs index 011d62c624..f34e09f68e 100644 --- a/backend/src/variables.rs +++ b/backend/src/variables.rs @@ -5,10 +5,13 @@ * LICENSE-AGPL for a copy of the license. */ +use std::sync::Arc; + use crate::{ audit::{audit_log, ActionKind}, db::{UserDB, DB}, error::{Error, JsonResult, Result}, + oauth2::{AllClients, _refresh_token}, users::Authed, utils::StripPath, }; @@ -20,6 +23,7 @@ use axum::{ use hyper::StatusCode; use magic_crypt::{MagicCrypt256, MagicCryptTrait}; +use reqwest::Client; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Postgres, Transaction}; @@ -51,6 +55,8 @@ pub struct ListableVariable { pub description: String, pub extra_perms: serde_json::Value, pub account: Option, + pub is_oauth: bool, + pub is_expired: Option, } #[derive(Deserialize)] @@ -59,6 +65,8 @@ pub struct CreateVariable { pub value: String, pub is_secret: bool, pub description: String, + pub account: Option, + pub is_oauth: Option, } #[derive(Deserialize)] @@ -152,7 +160,7 @@ async fn list_variables( let mut tx = user_db.begin(&authed).await?; let rows = sqlx::query_as::<_, ListableVariable>( - "SELECT workspace_id, path, CASE WHEN is_secret IS TRUE THEN null ELSE value::text END as value, is_secret, description, extra_perms, account from variable + "SELECT workspace_id, path, CASE WHEN is_secret IS TRUE THEN null ELSE value::text END as value, is_secret, description, extra_perms, account, is_oauth, false as is_expired from variable WHERE (workspace_id = $1 OR (is_secret IS NOT TRUE AND workspace_id = 'starter')) ORDER BY path", ) .bind(&w_id) @@ -173,12 +181,17 @@ async fn get_variable( Extension(user_db): Extension, Query(q): Query, Path((w_id, path)): Path<(String, StripPath)>, + Extension(clients): Extension>, + Extension(http_client): Extension, ) -> JsonResult { let path = path.to_path(); let mut tx = user_db.begin(&authed).await?; let variable_o = sqlx::query_as::<_, ListableVariable>( - "SELECT * from variable WHERE path = $1 AND (workspace_id = $2 OR (is_secret IS NOT TRUE AND workspace_id = 'starter'))", + "SELECT variable.*, (now() > account.expires_at) as is_expired from variable + LEFT JOIN account ON variable.account = account.id + WHERE variable.path = $1 AND (variable.workspace_id = $2 OR (is_secret IS NOT TRUE AND variable.workspace_id = 'starter')) + LIMIT 1", ) .bind(&path) .bind(&w_id) @@ -202,8 +215,22 @@ async fn get_variable( .await?; let value = variable.value.unwrap_or_else(|| "".to_string()); ListableVariable { - value: if !value.is_empty() && decrypt_secret { + value: if variable.is_expired.unwrap_or(false) && variable.account.is_some() { + Some( + _refresh_token( + tx, + &variable.path, + w_id, + variable.account.unwrap(), + clients, + http_client, + ) + .await?, + ) + } else if !value.is_empty() && decrypt_secret { let mc = build_crypt(&mut tx, &w_id).await?; + tx.commit().await?; + Some( mc.decrypt_base64_to_string(value) .map_err(|e| Error::InternalErr(e.to_string()))?, @@ -216,7 +243,6 @@ async fn get_variable( } else { variable }; - tx.commit().await?; Ok(Json(r)) } @@ -238,13 +264,15 @@ async fn create_variable( sqlx::query!( "INSERT INTO variable - (workspace_id, path, value, is_secret, description) - VALUES ($1, $2, $3, $4, $5)", + (workspace_id, path, value, is_secret, description, account, is_oauth) + VALUES ($1, $2, $3, $4, $5, $6, $7)", &w_id, variable.path, value, variable.is_secret, - variable.description + variable.description, + variable.account, + variable.is_oauth.unwrap_or(false), ) .execute(&mut tx) .await?; diff --git a/frontend/src/lib/components/AppConnect.svelte b/frontend/src/lib/components/AppConnect.svelte index 042a5a8439..6d682af800 100644 --- a/frontend/src/lib/components/AppConnect.svelte +++ b/frontend/src/lib/components/AppConnect.svelte @@ -13,18 +13,18 @@ import { workspaceStore, userStore, oauthStore } from '$lib/stores' import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons' - import { OauthService, ResourceService, VariableService } from '$lib/gen' + import { OauthService, ResourceService, VariableService, type TokenResponse } from '$lib/gen' import { createEventDispatcher, onMount } from 'svelte' import Modal from './Modal.svelte' import Icon from 'svelte-awesome' import Path from './Path.svelte' import Password from './Password.svelte' - import { sendUserToast, truncate, truncateRev } from '$lib/utils' - import { goto } from '$app/navigation' + import { sendUserToast, truncateRev } from '$lib/utils' let manual = false - let value = '' + let value: string = '' + let valueToken: TokenResponse let connects: Record = {} let connectsManual: [string, { img?: string; instructions: string }][] = [] @@ -46,7 +46,8 @@ export function openFromOauth(rt: string) { resource_type = rt - value = $oauthStore! + value = $oauthStore?.access_token! + valueToken = $oauthStore! $oauthStore = undefined manual = false step = 3 @@ -95,13 +96,30 @@ if (exists) { throw Error(`Resource at path ${path} already exists. Delete it or pick another path`) } + + let account: number | undefined = undefined + if (valueToken.refresh_token != undefined && valueToken.expires_in != undefined) { + account = Number( + await OauthService.createAccount({ + workspace: $workspaceStore!, + requestBody: { + refresh_token: valueToken.refresh_token!, + expires_in: valueToken.expires_in!, + owner: path.split('/').slice(0, 2).join('/'), + client: resource_type + } + }) + ) + } await VariableService.createVariable({ workspace: $workspaceStore!, requestBody: { path, value, is_secret: true, - description: `OAuth token for ${resource_type}` + description: `OAuth token for ${resource_type}`, + is_oauth: true, + account: account } }) await ResourceService.createResource({ @@ -110,7 +128,8 @@ resource_type, path, value: { token: `$var:${path}` }, - description: `OAuth token for ${resource_type}` + description: `OAuth token for ${resource_type}`, + is_oauth: true } }) dispatch('refresh') @@ -147,7 +166,7 @@
Connect an app
{#if step == 1} - +
{#each Object.entries(connects) as [key, values]}