fix: evict every alias a superadmin principal can be spelled as

This commit is contained in:
Ruben Fiszel
2026-08-04 07:22:52 +00:00
parent d2d5516e00
commit 47633f817b
2 changed files with 30 additions and 18 deletions
@@ -44,19 +44,18 @@ EXECUTE FUNCTION notify_usr_email_change();
CREATE OR REPLACE FUNCTION notify_superadmin_identity_change()
RETURNS TRIGGER AS $$
DECLARE
new_name TEXT;
old_name TEXT;
names TEXT[] := '{}';
BEGIN
IF TG_OP <> 'DELETE' THEN new_name := COALESCE(NEW.username, NEW.email); END IF;
IF TG_OP <> 'INSERT' THEN old_name := COALESCE(OLD.username, OLD.email); END IF;
IF new_name IS NOT NULL THEN
INSERT INTO notify_event (channel, payload)
VALUES ('notify_user_email_change', '*:' || new_name);
END IF;
IF old_name IS NOT NULL AND old_name IS DISTINCT FROM new_name THEN
INSERT INTO notify_event (channel, payload)
VALUES ('notify_user_email_change', '*:' || old_name);
END IF;
-- Both columns, not `COALESCE`: `resolve_username_to_email` matches a `u/` principal against
-- `username` OR `email`, and whichever string the caller passed is the key it cached under,
-- so an account with a username can still hold a live entry under its address. Old and new
-- of each, because a change to either leaves the other's entry behind.
IF TG_OP <> 'DELETE' THEN names := names || ARRAY[NEW.username, NEW.email]; END IF;
IF TG_OP <> 'INSERT' THEN names := names || ARRAY[OLD.username, OLD.email]; END IF;
INSERT INTO notify_event (channel, payload)
SELECT DISTINCT 'notify_user_email_change', '*:' || n
FROM unnest(names) AS n
WHERE n IS NOT NULL;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
+19 -6
View File
@@ -383,22 +383,27 @@ async fn test_trigger_notify_user_email_change(db: Pool<Postgres>) {
// A superadmin outside their workspaces resolves through `password`, which names no
// workspace: the wildcard is the only way to reach that key. `super_admin` is half of what
// that fallback matches on, so losing it moves the mapping just as the address does.
for (label, stmt) in [
for (label, stmt, expected_aliases) in [
(
"email change",
"UPDATE password SET email = 'sa2@windmill.dev' WHERE email = 'test@windmill.dev'",
// old address, new address, and the username that outlives both
vec!["test@windmill.dev", "sa2@windmill.dev", "test-user"],
),
(
"demotion",
"UPDATE password SET super_admin = false WHERE email = 'sa2@windmill.dev'",
vec!["sa2@windmill.dev", "test-user"],
),
(
"promotion",
"UPDATE password SET super_admin = true WHERE email = 'sa2@windmill.dev'",
vec!["sa2@windmill.dev", "test-user"],
),
(
"deletion",
"DELETE FROM password WHERE email = 'sa2@windmill.dev'",
vec!["sa2@windmill.dev", "test-user"],
),
] {
let before_id = get_latest_event_id(&db).await.unwrap();
@@ -410,11 +415,19 @@ async fn test_trigger_notify_user_email_change(db: Pool<Postgres>) {
let events = poll_notify_events(&db, before_id)
.await
.expect("Should poll events");
assert!(
events.iter().any(|e| e.channel == "notify_user_email_change"
&& e.payload.starts_with("*:")),
"superadmin {label} should emit a workspace-wildcard eviction for that name"
);
// Every alias the principal can be spelled as, since `resolve_username_to_email`
// matches a `u/` principal against the username or the address.
let evicted: Vec<&str> = events
.iter()
.filter(|e| e.channel == "notify_user_email_change")
.filter_map(|e| e.payload.strip_prefix("*:"))
.collect();
for alias in expected_aliases {
assert!(
evicted.contains(&alias),
"superadmin {label} should evict {alias}, got {evicted:?}"
);
}
}
}