feat: refactor variable + resource linkage + OAuth visibility

This commit is contained in:
Ruben Fiszel
2022-11-27 17:26:37 +01:00
parent f1446cd93e
commit 7c3126efc0
14 changed files with 544 additions and 298 deletions

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE resource DROP COLUMN is_oauth;

View File

@@ -1,96 +1,75 @@
{
"github": {
"auth_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
"scopes": [
"workflow",
"repo"
]
},
"gitlab": {
"auth_url": "https://gitlab.com/oauth/authorize",
"token_url": "https://gitlab.com/oauth/token",
"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",
"users:read",
"users:read.email"
]
},
"gsheets": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/spreadsheets"
],
"extra_params": {
"access_type": "offline",
"consent": "prompt"
}
},
"gdrive": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/drive"
],
"extra_params": {
"access_type": "offline",
"consent": "prompt"
}
},
"gmail": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/gmail.send"
],
"extra_params": {
"access_type": "offline",
"consent": "prompt"
}
},
"gcal": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/calendar.events"
],
"extra_params": {
"access_type": "offline",
"consent": "prompt"
}
},
"gcloud": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": [
"https://www.googleapis.com/auth/cloud-platform"
],
"extra_params": {
"access_type": "offline",
"consent": "prompt"
}
},
"basecamp": {
"auth_url": "https://launchpad.37signals.com/authorization/new",
"token_url": "https://launchpad.37signals.com/authorization/token",
"scopes": [],
"extra_params": {
"type": "web_server"
}
"github": {
"auth_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
"scopes": ["workflow", "repo"]
},
"gitlab": {
"auth_url": "https://gitlab.com/oauth/authorize",
"token_url": "https://gitlab.com/oauth/token",
"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", "users:read", "users:read.email"]
},
"gsheets": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": ["https://www.googleapis.com/auth/spreadsheets"],
"extra_params": {
"access_type": "offline",
"prompt": "consent"
}
},
"gdrive": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": ["https://www.googleapis.com/auth/drive"],
"extra_params": {
"access_type": "offline",
"prompt": "consent"
}
},
"gmail": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": ["https://www.googleapis.com/auth/gmail.send"],
"extra_params": {
"access_type": "offline",
"prompt": "consent"
}
},
"gcal": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": ["https://www.googleapis.com/auth/calendar.events"],
"extra_params": {
"access_type": "offline",
"prompt": "consent"
}
},
"gcloud": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"scopes": ["https://www.googleapis.com/auth/cloud-platform"],
"extra_params": {
"access_type": "offline",
"prompt": "consent"
}
},
"basecamp": {
"auth_url": "https://launchpad.37signals.com/authorization/new",
"token_url": "https://launchpad.37signals.com/authorization/token",
"scopes": [],
"extra_params": {
"type": "web_server"
}
}
}

View File

@@ -224,23 +224,6 @@
},
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, $4)"
},
"0c3a39eafc349870be019318d6925922558ac20fdd76b042d69ccd8a527e3ff5": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar",
"Bool"
]
}
},
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3"
},
"0d6412bc3ebb1d58bdd9cbcef774dacf9016fa402af5c1b4e339b9a3d7163d5e": {
"describe": {
"columns": [
@@ -678,23 +661,6 @@
},
"query": "SELECT now()"
},
"27eb5f99dc9289670673fb999ba9e67abccba212b506c30ba47e6c4fef6d53c4": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar",
"Bool"
]
}
},
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, is_oauth)\n VALUES ($1, $2, $3, $4, $5, $6)"
},
"282655dc568d000379196fae0dee32438a824d93f4ad2fcc155269e76f4ca20d": {
"describe": {
"columns": [],
@@ -1072,6 +1038,20 @@
},
"query": "UPDATE queue SET logs = concat(logs, $1::text) WHERE id = $2"
},
"493be465bf70411f933de8dd4da1c5b410fd5c79a0c17172e7882e292c1378b5": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3"
},
"499978e08a3a5e4d09d20f4212dbeff6ed5c5d4a696243cd3687c71099e8a6a6": {
"describe": {
"columns": [
@@ -1911,6 +1891,87 @@
},
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)"
},
"71b953191b3175a32a22f803205a5fd3dedef1baa0993afad08109ab5bf62fe9": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "value",
"ordinal": 2,
"type_info": "Jsonb"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "resource_type",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "extra_perms",
"ordinal": 5,
"type_info": "Jsonb"
},
{
"name": "is_expired",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "refresh_error",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "is_linked",
"ordinal": 8,
"type_info": "Bool"
},
{
"name": "is_oauth?",
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "account",
"ordinal": 10,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
true,
true,
false,
false,
null,
true,
null,
false,
true
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_error,\n variable.path IS NOT NULL as is_linked,\n variable.is_oauth as \"is_oauth?\",\n variable.account\n FROM resource\n LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id\n LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id\n WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')"
},
"765c18d77412cbb4474f4074d583b9b44681f3b9f58754662ac07a3a3470a3c5": {
"describe": {
"columns": [
@@ -2138,6 +2199,20 @@
},
"query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3"
},
"83fc9bf32b9b40987feff61fea82e20613e74efb7bf8ba52485566667ccf2284": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3"
},
"853788436dbe987853433e8dc83665f68bd127de31d4c807abafeead896f6ac4": {
"describe": {
"columns": [
@@ -2246,6 +2321,22 @@
},
"query": "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2"
},
"8a80333c2fbf7b50fed305882de6e4ffda985d5c648cd617add6c9e6a9c03f34": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar"
]
}
},
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5)"
},
"8ad6a17eecce77f61236e0585ba89b99a32e07ad37b97662007db35acb59d139": {
"describe": {
"columns": [
@@ -2485,63 +2576,6 @@
},
"query": "UPDATE password SET password_hash = $1 WHERE email = $2"
},
"904457944cbfdcefa1934059bbfea015a411c739124cf06367975cfd9cd0dd0d": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "value",
"ordinal": 2,
"type_info": "Jsonb"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "resource_type",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "extra_perms",
"ordinal": 5,
"type_info": "Jsonb"
},
{
"name": "is_oauth",
"ordinal": 6,
"type_info": "Bool"
}
],
"nullable": [
false,
false,
true,
true,
false,
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = 'starter')"
},
"90719f6230467b08e5f2cc89271bcf5e4a6cec39e9d9b42ef3b90f09b3135b83": {
"describe": {
"columns": [
@@ -3997,6 +4031,22 @@
},
"query": "DELETE FROM password WHERE email = $1"
},
"ea8ebb8d972fe99c960b5a69f794ee2b57bfb1914bf370c5b10313e45fa9b65f": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Varchar"
]
}
},
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type)\n VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3"
},
"ed400dbf4a1fa597bc984240cfe9069369a8a265b127116187d78c0ba93ae076": {
"describe": {
"columns": [
@@ -4374,11 +4424,6 @@
"name": "extra_perms",
"ordinal": 5,
"type_info": "Jsonb"
},
{
"name": "is_oauth",
"ordinal": 6,
"type_info": "Bool"
}
],
"nullable": [
@@ -4387,7 +4432,6 @@
true,
true,
false,
false,
false
],
"parameters": {

View File

@@ -1331,7 +1331,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Resource"
$ref: "#/components/schemas/ListableResource"
/w/{workspace}/resources/get_value/{path}:
get:
@@ -1389,7 +1389,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/Resource"
$ref: "#/components/schemas/ListableResource"
/w/{workspace}/resources/type/create:
post:
@@ -4132,6 +4132,8 @@ components:
type: boolean
refresh_error:
type: string
is_linked:
type: boolean
required:
- workspace_id
- path
@@ -4366,8 +4368,6 @@ components:
type: string
resource_type:
type: string
is_oauth:
type: boolean
required:
- path
- value
@@ -4405,6 +4405,38 @@ components:
- resource_type
- is_oauth
ListableResource:
type: object
properties:
workspace_id:
type: string
path:
type: string
description:
type: string
resource_type:
type: string
value: {}
is_oauth:
type: boolean
extra_perms:
type: object
additionalProperties:
type: boolean
is_expired:
type: boolean
refresh_error:
type: string
is_linked:
type: boolean
account:
type: number
required:
- path
- resource_type
- is_oauth
- is_linked
ResourceType:
type: object
properties:

View File

@@ -514,7 +514,7 @@ pub async fn _refresh_token<'c>(
if let Err(token_err) = token {
sqlx::query!(
"UPDATE account SET refresh_error = $1 WHERE workspace_id = $2 AND id = $3",
token_err.to_string(),
token_err.alt(),
w_id,
id,
)
@@ -522,8 +522,8 @@ pub async fn _refresh_token<'c>(
.await?;
tx.commit().await?;
return Err(error::Error::BadRequest(format!(
"Error refreshing token: {:#?}",
token_err
"Error refreshing token: {}",
token_err.alt()
)));
};
@@ -676,14 +676,13 @@ async fn connect_slack_callback(
sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, description, resource_type, is_oauth)
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3",
(workspace_id, path, value, description, resource_type)
VALUES ($1, $2, $3, $4, $5) ON CONFLICT (workspace_id, path) DO UPDATE SET value = $3",
w_id,
token_path,
serde_json::json!({ "token": format!("$var:{token_path}") }),
"The slack bot token to act on behalf of the installed app of the connected workspace",
"slack",
true
)
.execute(&mut tx)
.await?;

View File

@@ -72,7 +72,21 @@ pub struct Resource {
pub description: Option<String>,
pub resource_type: String,
pub extra_perms: serde_json::Value,
pub is_oauth: bool,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct ListableResource {
pub workspace_id: String,
pub path: String,
pub value: Option<serde_json::Value>,
pub description: Option<String>,
pub resource_type: String,
pub extra_perms: serde_json::Value,
pub is_linked: Option<bool>,
pub is_oauth: Option<bool>,
pub is_expired: Option<bool>,
pub refresh_error: Option<String>,
pub account: Option<i32>,
}
#[derive(Deserialize)]
@@ -81,7 +95,6 @@ pub struct CreateResource {
pub value: Option<serde_json::Value>,
pub description: Option<String>,
pub resource_type: String,
pub is_oauth: Option<bool>,
}
#[derive(Deserialize)]
struct EditResource {
@@ -100,31 +113,42 @@ async fn list_resources(
Query(pagination): Query<Pagination>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
) -> JsonResult<Vec<Resource>> {
) -> JsonResult<Vec<ListableResource>> {
let (per_page, offset) = paginate(pagination);
let mut sqlb = SqlBuilder::select_from("resource")
.fields(&[
"workspace_id",
"path",
"resource.workspace_id",
"resource.path",
"null::JSONB as value",
"description",
"resource.description",
"resource_type",
"extra_perms",
"is_oauth",
"resource.extra_perms",
"(now() > account.expires_at) as is_expired",
"variable.path IS NOT NULL as is_linked",
"variable.is_oauth",
"variable.account",
"account.refresh_error",
])
.left()
.join("variable")
.on("variable.path = resource.path AND variable.workspace_id = resource.workspace_id")
.left()
.join("account")
.on("variable.account = account.id AND account.workspace_id = variable.workspace_id")
.order_by("path", true)
.and_where("workspace_id = ? OR workspace_id = 'starter'".bind(&w_id))
.and_where("resource.workspace_id = ? OR resource.workspace_id = 'starter'".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
if let Some(rt) = &lq.resource_type {
sqlb.and_where_eq("resource_type", "?".bind(rt));
}
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, Resource>(&sql)
let rows = sqlx::query_as::<_, ListableResource>(&sql)
.fetch_all(&mut tx)
.await?;
@@ -137,14 +161,20 @@ async fn get_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Resource> {
) -> JsonResult<ListableResource> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let resource_o = sqlx::query_as!(
Resource,
"SELECT * from resource WHERE path = $1 AND (workspace_id = $2 OR workspace_id = \
'starter')",
ListableResource,
"SELECT resource.*, (now() > account.expires_at) as is_expired, account.refresh_error,
variable.path IS NOT NULL as is_linked,
variable.is_oauth as \"is_oauth?\",
variable.account
FROM resource
LEFT JOIN variable ON variable.path = resource.path AND variable.workspace_id = resource.workspace_id
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = resource.workspace_id
WHERE resource.path = $1 AND (resource.workspace_id = $2 OR resource.workspace_id = 'starter')",
path.to_owned(),
&w_id
)
@@ -206,14 +236,13 @@ async fn create_resource(
sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, description, resource_type, is_oauth)
VALUES ($1, $2, $3, $4, $5, $6)",
(workspace_id, path, value, description, resource_type)
VALUES ($1, $2, $3, $4, $5)",
w_id,
resource.path,
resource.value,
resource.description,
resource.resource_type,
resource.is_oauth.unwrap_or(false)
)
.execute(&mut tx)
.await?;
@@ -250,6 +279,13 @@ async fn delete_resource(
)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM variable WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
@@ -298,6 +334,17 @@ async fn update_resource(
let npath = not_found_if_none(npath_o, "Resource", path)?;
if let Some(npath) = ns.path {
sqlx::query!(
"UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
path,
w_id
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,

View File

@@ -82,10 +82,13 @@ async fn list_variables(
let mut tx = user_db.begin(&authed).await?;
let rows = sqlx::query_as::<_, ListableVariable>(
"SELECT variable.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, (now() > account.expires_at) as is_expired,
account.refresh_error from variable
"SELECT variable.workspace_id, variable.path, CASE WHEN is_secret IS TRUE THEN null ELSE variable.value::text END as value,
is_secret, variable.description, variable.extra_perms, account, is_oauth, (now() > account.expires_at) as is_expired,
account.refresh_error,
resource.path IS NOT NULL as is_linked
from variable
LEFT JOIN account ON variable.account = account.id AND account.workspace_id = variable.workspace_id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
WHERE variable.workspace_id = $1 OR (is_secret IS NOT TRUE AND variable.workspace_id = 'starter') ORDER BY path",
)
.bind(&w_id)
@@ -113,8 +116,11 @@ async fn get_variable(
let mut tx = user_db.begin(&authed).await?;
let variable_o = sqlx::query_as::<_, ListableVariable>(
"SELECT variable.*, (now() > account.expires_at) as is_expired, account.refresh_error from variable
"SELECT variable.*, (now() > account.expires_at) as is_expired, account.refresh_error,
resource.path IS NOT NULL as is_linked
from variable
LEFT JOIN account ON variable.account = account.id
LEFT JOIN resource ON resource.path = variable.path AND resource.workspace_id = variable.workspace_id
WHERE variable.path = $1 AND (variable.workspace_id = $2 OR (is_secret IS NOT TRUE AND \
variable.workspace_id = 'starter'))
LIMIT 1",
@@ -255,6 +261,13 @@ async fn delete_variable(
)
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2",
path,
w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
@@ -335,6 +348,17 @@ async fn update_variable(
let npath_o: Option<String> = sqlx::query_scalar(&sql).fetch_optional(&mut tx).await?;
if let Some(npath) = ns.path {
sqlx::query!(
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,
path,
w_id
)
.execute(&mut tx)
.await?;
}
let npath = not_found_if_none(npath_o, "Variable", path)?;
audit_log(

View File

@@ -27,9 +27,10 @@ pub struct ListableVariable {
pub description: String,
pub extra_perms: serde_json::Value,
pub account: Option<i32>,
pub is_oauth: bool,
pub is_oauth: Option<bool>,
pub is_expired: Option<bool>,
pub refresh_error: Option<String>,
pub is_linked: Option<bool>,
}
#[derive(Deserialize)]

View File

@@ -316,17 +316,17 @@ async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyho
}
#[op]
async fn op_resource(
args: Vec<String>,
) -> Result<windmill_api_client::types::Resource, anyhow::Error> {
async fn op_resource(args: Vec<String>) -> Result<serde_json::Value, anyhow::Error> {
let workspace = &args[0];
let path = &args[1];
let token = &args[2];
let base_url = &args[3];
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client.get_resource(workspace, path).await?;
// TODO: verify this works. Previously this returned Option<serde_jons::Value>, now it's statically typed.
Ok(result.into_inner())
Ok(result
.into_inner()
.value
.unwrap_or_else(|| serde_json::json!({})))
}
#[cfg(test)]

View File

@@ -151,8 +151,7 @@
resource_type,
path,
value: resourceValue,
description,
is_oauth: !manual
description
}
})
dispatch('refresh')

View File

@@ -155,7 +155,8 @@
{#if edit && variable.is_secret}<Button
variant="border"
size="xs"
on:click={() => loadVariable(initialPath)}>Load secret value</Button
on:click={() => loadVariable(initialPath)}
>Load secret value<Tooltip>Will generate an audit log</Tooltip></Button
>{/if}
</div>
<div class="flex flex-row">

View File

@@ -8,7 +8,7 @@
<script lang="ts">
import { canWrite, emptySchema, sendUserToast, truncate } from '$lib/utils'
import { ResourceService, VariableService } from '$lib/gen'
import { OauthService, ResourceService, VariableService, type ListableResource } from '$lib/gen'
import type { Resource, ResourceType } from '$lib/gen'
import PageHeader from '$lib/components/PageHeader.svelte'
import ResourceEditor from '$lib/components/ResourceEditor.svelte'
@@ -31,7 +31,8 @@
faTrash,
faCircle,
faChain,
faSave
faSave,
faRefresh
} from '@fortawesome/free-solid-svg-icons'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
@@ -47,7 +48,7 @@
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
type ResourceW = Resource & { canWrite: boolean }
type ResourceW = ListableResource & { canWrite: boolean }
type ResourceTypeW = ResourceType & { canWrite: boolean }
let resources: ResourceW[] | undefined
@@ -94,9 +95,9 @@
loading.types = false
}
async function deleteResource(path: string, is_oauth: boolean): Promise<void> {
if (is_oauth) {
await VariableService.deleteVariable({ workspace: $workspaceStore!, path })
async function deleteResource(path: string, account?: number): Promise<void> {
if (account) {
OauthService.disconnectAccount({ workspace: $workspaceStore!, id: account })
}
await ResourceService.deleteResource({ workspace: $workspaceStore!, path })
loadResources()
@@ -253,12 +254,12 @@
<th>path</th>
<th>resource type</th>
<th>description</th>
<th>OAuth</th>
<th />
<th />
</tr>
<tbody slot="body">
{#if resources}
{#each resources as { path, description, resource_type, extra_perms, canWrite, is_oauth }}
{#each resources as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired }}
<tr>
<td class="my-12"
><a
@@ -285,20 +286,74 @@
></td
>
<td class="text-center">
{#if is_oauth}
<Popover>
<Icon
class="text-green-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Variable is tied to an OAuth app"
/>
<div slot="text">
The resource is tied to an OAuth app. The token is refreshed automatically
if applicable.
<div class="flex flex-row">
<div class="w-10">
{#if is_linked}
<Popover>
<Icon data={faChain} />
<div slot="text">
This resource is linked with a variable of the same path. They are
deleted and renamed together.
</div>
</Popover>
{/if}
</div>
<div class="w-10">
{#if account}
<Popover>
<Icon data={faRefresh} />
<div slot="text">
The OAuth token is kept up-to-date in the background by Windmill using
its refresh token
</div>
</Popover>
{/if}
</div>
{#if is_oauth}
<div class="w-10">
{#if refresh_error}
<Popover>
<Icon
class="text-red-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Error during exchange of the refresh token"
/>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
</Popover>
{:else if is_expired}
<Popover>
<Icon
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Variable is expired"
/>
<div slot="text">
The access_token is expired, it will get renewed the next time this
variable is fetched or you can request is to be refreshed in the
dropdown on the right.
</div>
</Popover>
{:else}
<Popover>
<Icon
class="text-green-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Variable is tied to an OAuth app"
/>
<div slot="text">
The resource was connected through OAuth and the token is not expired.
</div>
</Popover>
{/if}
</div>
</Popover>
{/if}
{/if}
</div>
</td>
<td>
<Dropdown
@@ -326,14 +381,33 @@
type: 'delete',
action: (event) => {
if (event?.shiftKey) {
deleteResource(path, is_oauth)
deleteResource(path, account)
} else {
deleteConfirmedCallback = () => {
deleteResource(path, is_oauth)
deleteResource(path, account)
}
}
}
}
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: faRefresh,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account ?? 0,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
loadResources()
}
}
]
: [])
]}
relative={true}
/>

View File

@@ -20,7 +20,18 @@
import { userStore, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
import { faPlus, faCircle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
import {
faPlus,
faCircle,
faEyeSlash,
faRotateRight,
faArrowRotateRight,
faRefresh,
faChain,
faTrash,
faEdit,
faShare
} from '@fortawesome/free-solid-svg-icons'
import { Button } from '$lib/components/common'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import { Alert, Badge, Skeleton } from '$lib/components/common'
@@ -98,11 +109,11 @@
<th>value</th>
<th>description</th>
<th>OAuth</th>
<th />
<th />
</tr>
<tbody slot="body">
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth, is_expired, refresh_error }}
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth, is_expired, refresh_error, is_linked }}
<tr>
<td
><a
@@ -136,59 +147,88 @@
>
<td class="text-center">
{#if is_oauth}
{#if refresh_error}
<Popover>
<Icon
class="text-red-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Error during exchange of the refresh token"
/>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
</Popover>
{:else if is_expired}
<Popover>
<Icon
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Variable is expired"
/>
<div slot="text">
The access_token is expired, it will get renewed the next time this variable
is fetched or you can request is to be refreshed in the variable dropdown.
</div>
</Popover>
{:else}
<Popover>
<Icon
class="text-green-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Variable is tied to an OAuth app"
/>
<div slot="text">
The variable is tied to an OAuth app. The token is refreshed automatically
if applicable.
</div>
</Popover>
<div class="flex flex-row">
<div class="w-10">
{#if is_linked}
<Popover>
<Icon data={faChain} />
<div slot="text">
This variable is linked with a resource of the same path. They are deleted
and renamed together.
</div>
</Popover>
{/if}
</div>
<div class="w-10">
{#if account}
<Popover>
<Icon data={faRefresh} />
<div slot="text">
This OAuth token is kept up-to-date in the background by Windmill using
its refresh token
</div>
</Popover>
{/if}
</div>
{#if is_oauth}
<div class="w-10">
{#if refresh_error}
<Popover>
<Icon
class="text-red-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Error during exchange of the refresh token"
/>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
</Popover>
{:else if is_expired}
<Popover>
<Icon
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Variable is expired"
/>
<div slot="text">
The access_token is expired, it will get renewed the next time this
variable is fetched or you can request is to be refreshed in the
dropdown on the right.
</div>
</Popover>
{:else}
<Popover>
<Icon
class="text-green-600 animate-[pulse_5s_linear_infinite]"
data={faCircle}
scale={0.7}
label="Variable is tied to an OAuth app"
/>
<div slot="text">
The variable was connected through OAuth and the token is not expired.
</div>
</Popover>
{/if}
</div>
{/if}
{/if}
</div>
</td>
<td
><Dropdown
dropdownItems={[
{
displayName: 'Edit',
icon: faEdit,
action: () => variableEditor.editVariable(path),
disabled: !canWrite
},
{
displayName: 'Delete',
icon: faTrash,
type: 'delete',
action: (event) => {
if (event?.shiftKey) {
deleteVariable(path, account)
@@ -205,12 +245,14 @@
action: () => {
shareModal.openDrawer(path)
},
icon: faShare,
disabled: !canWrite
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: faRefresh,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
@@ -220,6 +262,7 @@
}
})
sendUserToast('Token refreshed')
loadVariables()
}
}
]