Compare commits

...

2 Commits

Author SHA1 Message Date
Ruben Fiszel
f6b29694d2 fix: add pre-fix migration to correct edited_by before permissioned_as migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 08:34:24 +00:00
Ruben Fiszel
107dfee8ec fix: resolve trigger 404 when permissioned_as user missing from workspace
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 01:47:45 +00:00
10 changed files with 222 additions and 43 deletions

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM password WHERE username = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "0d75c9bd7f532622c80cc856e9ee48923a97b5db7130e6517bee3ea90cc24ef5"
}

View File

@@ -1,30 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n filter_logic = $5,\n auto_offset_reset = $6,\n auto_commit = $7,\n script_path = $8,\n path = $9,\n is_flow = $10,\n edited_by = $11,\n permissioned_as = $12,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $15,\n error_handler_args = $16,\n retry = $17\n WHERE\n workspace_id = $13 AND path = $14\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"JsonbArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "68c19cb0e18b94870bbe81f9aab92ba37da67cd2a56834c9d1378eab7551284d"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n auto_invite->'instance_groups_roles' as instance_groups_roles,\n auto_invite->'instance_groups' as instance_groups_json\n FROM workspace_settings\n WHERE\n auto_invite->'instance_groups' IS NOT NULL\n AND auto_invite->'instance_groups' ? $1\n ",
"query": "\n SELECT\n workspace_id,\n auto_invite->'instance_groups_roles' as instance_groups_roles\n FROM workspace_settings\n WHERE\n auto_invite->'instance_groups' IS NOT NULL\n AND auto_invite->'instance_groups' ? $1\n ",
"describe": {
"columns": [
{
@@ -12,11 +12,6 @@
"ordinal": 1,
"name": "instance_groups_roles",
"type_info": "Jsonb"
},
{
"ordinal": 2,
"name": "instance_groups_json",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -26,9 +21,8 @@
},
"nullable": [
false,
null,
null
]
},
"hash": "66e2f8468ba64f22b7a7caa18639d7c833ac2ec573bd89d878b5c8b1afc74d3a"
"hash": "7e01ef5799168c0fc2779d42ce352827e2fda6711c0a1b104ca6435ddb14b47d"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n filter_logic,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, now(), $15, $16, $17\n )\n ",
"query": "\n INSERT INTO kafka_trigger (\n workspace_id,\n path,\n kafka_resource_path,\n group_id,\n topics,\n filters,\n auto_offset_reset,\n auto_commit,\n script_path,\n is_flow,\n mode,\n edited_by,\n permissioned_as,\n edited_at,\n error_handler_path,\n error_handler_args,\n retry\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, now(), $14, $15, $16\n )\n ",
"describe": {
"columns": [],
"parameters": {
@@ -12,7 +12,6 @@
"VarcharArray",
"JsonbArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
@@ -37,5 +36,5 @@
},
"nullable": []
},
"hash": "6a8f4ed9946bb2a3c5e90695c90b70aa2e83fcb5aa0c953febdd9bac2d95bbec"
"hash": "a0a545fda5f3ebea0113d5daaf13358c964d9fb0f41bf2a1c834305b4d2398f2"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE kafka_trigger\n SET\n kafka_resource_path = $1,\n group_id = $2,\n topics = $3,\n filters = $4,\n auto_offset_reset = $5,\n auto_commit = $6,\n script_path = $7,\n path = $8,\n is_flow = $9,\n edited_by = $10,\n permissioned_as = $11,\n edited_at = now(),\n server_id = NULL,\n error = NULL,\n error_handler_path = $14,\n error_handler_args = $15,\n retry = $16\n WHERE\n workspace_id = $12 AND path = $13\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"JsonbArray",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Text",
"Text",
"Varchar",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "a37cfc632dd37cf37c06743239b5ebc784e5da5ee25d47af187a75220d8fded7"
}

View File

@@ -0,0 +1 @@
-- No-op: this migration is a data fixup and cannot be reversed.

View File

@@ -0,0 +1,48 @@
-- Pre-fix: before permissioned_as migration drops the email column, update edited_by
-- for triggers where the user (edited_by) is not in the workspace but is a superadmin.
-- This ensures the subsequent 20260318000000 migration stores the raw email as permissioned_as
-- (via the `edited_by LIKE '%@%'` branch).
-- For instances that already applied 20260318000000, this is a no-op (email column is gone);
-- the 20260401000000 migration handles those as a fallback.
DO $$
DECLARE
trigger_table TEXT;
has_email BOOLEAN;
BEGIN
FOREACH trigger_table IN ARRAY ARRAY[
'http_trigger',
'websocket_trigger',
'postgres_trigger',
'mqtt_trigger',
'kafka_trigger',
'nats_trigger',
'sqs_trigger',
'gcp_trigger',
'email_trigger'
]
LOOP
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = trigger_table AND column_name = 'email'
) INTO has_email;
IF has_email THEN
EXECUTE format($q$
UPDATE %I t
SET edited_by = t.email
WHERE NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = t.edited_by
AND u.workspace_id = t.workspace_id
)
AND EXISTS (
SELECT 1 FROM password p
WHERE p.email = t.email
AND p.super_admin = true
)
$q$, trigger_table);
END IF;
END LOOP;
END;
$$;

View File

@@ -0,0 +1 @@
-- No-op: the forward migration only fixes stale data; reverting is not meaningful.

View File

@@ -0,0 +1,103 @@
-- Fix triggers/schedules where permissioned_as = 'u/{username}' but the user
-- does not exist in the workspace's usr table. Replace with the raw email
-- from the instance-level password table so the super-admin check succeeds.
UPDATE http_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE websocket_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE postgres_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE mqtt_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE kafka_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE nats_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE sqs_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE gcp_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE email_trigger t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);
UPDATE schedule t SET permissioned_as = p.email
FROM password p
WHERE t.permissioned_as LIKE 'u/%'
AND p.username = SUBSTRING(t.permissioned_as FROM 3)
AND NOT EXISTS (
SELECT 1 FROM usr u
WHERE u.username = SUBSTRING(t.permissioned_as FROM 3)
AND u.workspace_id = t.workspace_id
);

View File

@@ -68,8 +68,20 @@ pub async fn get_email_from_permissioned_as(
workspace_id
)
.fetch_optional(db)
.await?
.unwrap_or_else(|| format!("{}@unknown.windmill.dev", username));
.await?;
let email = match email {
Some(e) => e,
None => {
// User not in workspace — check instance-level password table.
// This handles super admins who can access any workspace without
// a usr record; without the real email, the super_admin check in
// fetch_authed_from_permissioned_as would fail.
sqlx::query_scalar!("SELECT email FROM password WHERE username = $1", username)
.fetch_optional(db)
.await?
.unwrap_or_else(|| format!("{}@unknown.windmill.dev", username))
}
};
let key = (workspace_id.to_string(), username.to_string());
EMAIL_CACHE.insert(key, (email.clone(), std::time::Instant::now()));
Ok(email)