fix: delete non-session tokens on workspace archive and reject token creation for archived workspaces (#8082)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
22
backend/.sqlx/query-2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91.json
generated
Normal file
22
backend/.sqlx/query-2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM token WHERE workspace_id = $1 AND label IS DISTINCT FROM 'session' RETURNING token",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91"
|
||||
}
|
||||
19
backend/.sqlx/query-d32448f6b329cf98dad42b218a630c0cf40a99edb4ae9fe3e9be485ab1077b3a.json
generated
Normal file
19
backend/.sqlx/query-d32448f6b329cf98dad42b218a630c0cf40a99edb4ae9fe3e9be485ab1077b3a.json
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO token (token, email, label, expiration, scopes, workspace_id)\n SELECT $1::varchar, $2::varchar, $3::varchar, now() + ($4 || ' seconds')::interval, $5::text[], $6::varchar\n WHERE NOT EXISTS(SELECT 1 FROM workspace WHERE id = $6 AND deleted = true)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d32448f6b329cf98dad42b218a630c0cf40a99edb4ae9fe3e9be485ab1077b3a"
|
||||
}
|
||||
20
backend/.sqlx/query-e33be0991702ae3a295db7defc6d19d914307a95d72bb0fb447e5b367d52f6a0.json
generated
Normal file
20
backend/.sqlx/query-e33be0991702ae3a295db7defc6d19d914307a95d72bb0fb447e5b367d52f6a0.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes, workspace_id)\n SELECT $1, $2, $3, $4, $5, $6, $7\n WHERE $7::varchar IS NULL OR NOT EXISTS(\n SELECT 1 FROM workspace WHERE id = $7 AND deleted = true\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"TextArray",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e33be0991702ae3a295db7defc6d19d914307a95d72bb0fb447e5b367d52f6a0"
|
||||
}
|
||||
@@ -534,10 +534,13 @@ pub async fn create_token_internal(
|
||||
));
|
||||
}
|
||||
}
|
||||
sqlx::query!(
|
||||
let rows = sqlx::query!(
|
||||
"INSERT INTO token
|
||||
(token, email, label, expiration, super_admin, scopes, workspace_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE $7::varchar IS NULL OR NOT EXISTS(
|
||||
SELECT 1 FROM workspace WHERE id = $7 AND deleted = true
|
||||
)",
|
||||
token,
|
||||
authed.email,
|
||||
token_config.label,
|
||||
@@ -548,6 +551,11 @@ pub async fn create_token_internal(
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
if rows.rows_affected() == 0 {
|
||||
return Err(Error::BadRequest(
|
||||
"Cannot create a token for an archived workspace".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
|
||||
@@ -3558,7 +3558,7 @@ pub(crate) async fn archive_workspace_impl(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
username: &str,
|
||||
) -> Result<(usize, usize)> {
|
||||
) -> Result<(usize, usize, usize)> {
|
||||
// Step 1: Disable all schedules and clear their queued jobs
|
||||
let mut tx = db.begin().await?;
|
||||
let disabled_schedules = sqlx::query_scalar!(
|
||||
@@ -3580,6 +3580,20 @@ pub(crate) async fn archive_workspace_impl(
|
||||
windmill_queue::schedule::clear_schedule(&mut tx, schedule_path, w_id).await?;
|
||||
}
|
||||
|
||||
// Delete non-session tokens scoped to this workspace
|
||||
let deleted_tokens = sqlx::query_scalar!(
|
||||
"DELETE FROM token WHERE workspace_id = $1 AND label IS DISTINCT FROM 'session' RETURNING token",
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
"Deleted {} non-session tokens in workspace {}",
|
||||
deleted_tokens.len(),
|
||||
w_id
|
||||
);
|
||||
|
||||
// Mark workspace as archived
|
||||
sqlx::query!("UPDATE workspace SET deleted = true WHERE id = $1", w_id)
|
||||
.execute(&mut *tx)
|
||||
@@ -3618,7 +3632,7 @@ pub(crate) async fn archive_workspace_impl(
|
||||
0
|
||||
};
|
||||
|
||||
Ok((schedules_count, canceled_count))
|
||||
Ok((schedules_count, canceled_count, deleted_tokens.len()))
|
||||
}
|
||||
|
||||
async fn archive_workspace(
|
||||
@@ -3628,7 +3642,7 @@ async fn archive_workspace(
|
||||
) -> Result<String> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let (schedules_count, canceled_count) =
|
||||
let (schedules_count, canceled_count, deleted_tokens_count) =
|
||||
archive_workspace_impl(&db, &w_id, &authed.username).await?;
|
||||
|
||||
// Audit log
|
||||
@@ -3636,6 +3650,7 @@ async fn archive_workspace(
|
||||
let mut audit_params = HashMap::new();
|
||||
audit_params.insert("disabled_schedules", schedules_count.to_string());
|
||||
audit_params.insert("canceled_jobs", canceled_count.to_string());
|
||||
audit_params.insert("deleted_tokens", deleted_tokens_count.to_string());
|
||||
let audit_params_refs: HashMap<&str, &str> =
|
||||
audit_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
|
||||
|
||||
@@ -3652,8 +3667,8 @@ async fn archive_workspace(
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!(
|
||||
"Archived workspace {}, disabled {} schedules and canceled {} jobs",
|
||||
&w_id, schedules_count, canceled_count
|
||||
"Archived workspace {}, disabled {} schedules, canceled {} jobs and deleted {} tokens",
|
||||
&w_id, schedules_count, canceled_count, deleted_tokens_count
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -641,7 +641,7 @@ pub(crate) async fn change_workspace_id(
|
||||
// Archive old workspace: disable schedules, cancel remaining jobs, set deleted=true
|
||||
// Note: schedules were already moved to new workspace, so this will find 0 schedules
|
||||
info!("Archiving old workspace");
|
||||
let (_schedules_count, canceled_count) =
|
||||
let (_schedules_count, canceled_count, _deleted_tokens_count) =
|
||||
archive_workspace_impl(&db, &old_id, &authed.username).await?;
|
||||
|
||||
info!(
|
||||
|
||||
@@ -387,10 +387,11 @@ async fn handle_authorization_code_grant(
|
||||
let token_family = sqlx::types::Uuid::new_v4();
|
||||
let scopes = auth_code.scopes;
|
||||
|
||||
// Create access token
|
||||
if let Err(e) = sqlx::query!(
|
||||
// Create access token (rejects archived workspaces inline)
|
||||
let rows = sqlx::query!(
|
||||
"INSERT INTO token (token, email, label, expiration, scopes, workspace_id)
|
||||
VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5, $6)",
|
||||
SELECT $1::varchar, $2::varchar, $3::varchar, now() + ($4 || ' seconds')::interval, $5::text[], $6::varchar
|
||||
WHERE NOT EXISTS(SELECT 1 FROM workspace WHERE id = $6 AND deleted = true)",
|
||||
access_token,
|
||||
auth_code.user_email,
|
||||
format!("mcp-oauth-{}", auth_code.client_id),
|
||||
@@ -400,10 +401,13 @@ async fn handle_authorization_code_grant(
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to create access token: {}", e);
|
||||
return Err(OAuthTokenError::server_error(
|
||||
"Failed to create access token",
|
||||
OAuthTokenError::server_error("Failed to create access token")
|
||||
})?;
|
||||
if rows.rows_affected() == 0 {
|
||||
return Err(OAuthTokenError::invalid_grant(
|
||||
"Cannot create a token for an archived workspace",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -514,10 +518,11 @@ async fn handle_refresh_token_grant(
|
||||
let new_refresh_token = rd_string(32);
|
||||
let scopes = token_row.scopes;
|
||||
|
||||
// Create new access token
|
||||
if let Err(e) = sqlx::query!(
|
||||
// Create new access token (rejects archived workspaces inline)
|
||||
let rows = sqlx::query!(
|
||||
"INSERT INTO token (token, email, label, expiration, scopes, workspace_id)
|
||||
VALUES ($1, $2, $3, now() + ($4 || ' seconds')::interval, $5, $6)",
|
||||
SELECT $1::varchar, $2::varchar, $3::varchar, now() + ($4 || ' seconds')::interval, $5::text[], $6::varchar
|
||||
WHERE NOT EXISTS(SELECT 1 FROM workspace WHERE id = $6 AND deleted = true)",
|
||||
new_access_token,
|
||||
token_row.user_email,
|
||||
format!("mcp-oauth-{}", token_row.client_id),
|
||||
@@ -527,10 +532,13 @@ async fn handle_refresh_token_grant(
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to create new access token: {}", e);
|
||||
return Err(OAuthTokenError::server_error(
|
||||
"Failed to create access token",
|
||||
OAuthTokenError::server_error("Failed to create access token")
|
||||
})?;
|
||||
if rows.rows_affected() == 0 {
|
||||
return Err(OAuthTokenError::invalid_grant(
|
||||
"Cannot create a token for an archived workspace",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user