Skip to content

Commit 215426a

Browse files
committed
fix snapshots grant filtering to account for sso provider id
1 parent cdc3048 commit 215426a

File tree

6 files changed

+96
-192
lines changed

6 files changed

+96
-192
lines changed

.sqlx/query-e60eb5d6aec7b22576ff88a5862784cae60b21aa971908b01eb16ff4770bf29b.json renamed to .sqlx/query-33c43cf8dd44d4e448fdac5a9f128b11d833f6ab21e90f89bbf5dce433a4293d.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.sqlx/query-59b5d98e5cc0ce441ddefa0b32eef6820b1db95156c6206e773362e4477fc2dd.json

Lines changed: 0 additions & 94 deletions
This file was deleted.

.sqlx/query-a5697a9097f21c69f3da2850fd08cd9aa7102b964071bad56e9b82bb9e82bce1.json

Lines changed: 0 additions & 94 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
-- Sets up SSO-enforced tenants with users whose identities do/don't match.
2+
-- Alice has an SSO identity matching acmeCo's provider — her grant should be included.
3+
-- Bob has an SSO identity for a *different* provider — his grant should be excluded.
4+
-- Carol has no SSO identity at all — her grant should be excluded.
5+
-- All three have grants on openCo (no SSO enforcement) — those should be included.
6+
do $$
7+
declare
8+
provider_acme uuid := 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
9+
provider_other uuid := 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
10+
11+
alice_uid uuid := '11111111-1111-1111-1111-111111111111';
12+
bob_uid uuid := '22222222-2222-2222-2222-222222222222';
13+
carol_uid uuid := '33333333-3333-3333-3333-333333333333';
14+
begin
15+
16+
insert into auth.sso_providers (id) values (provider_acme), (provider_other);
17+
18+
insert into auth.users (id, email, is_sso_user) values
19+
(alice_uid, 'alice@acme.co', true),
20+
(bob_uid, 'bob@other.co', true),
21+
(carol_uid, 'carol@example.com', false)
22+
;
23+
24+
-- Alice has the correct SSO identity for acmeCo's provider.
25+
insert into auth.identities (user_id, provider, provider_id, identity_data) values
26+
(alice_uid, 'sso', provider_acme::text, '{}'::jsonb),
27+
(bob_uid, 'sso', provider_other::text, '{}'::jsonb)
28+
;
29+
30+
insert into public.tenants (tenant, sso_provider_id, enforce_sso) values
31+
('acmeCo/', provider_acme, true),
32+
('openCo/', null, false)
33+
;
34+
35+
insert into public.user_grants (user_id, object_role, capability) values
36+
(alice_uid, 'acmeCo/', 'admin'),
37+
(bob_uid, 'acmeCo/', 'admin'),
38+
(carol_uid, 'acmeCo/', 'admin'),
39+
(alice_uid, 'openCo/', 'read'),
40+
(bob_uid, 'openCo/', 'read'),
41+
(carol_uid, 'openCo/', 'read')
42+
;
43+
44+
end
45+
$$;

crates/control-plane-api/src/server/snapshot.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,12 @@ pub async fn try_fetch(
500500
SELECT 1 FROM tenants t
501501
WHERE t.tenant ^@ g.object_role
502502
AND t.enforce_sso
503-
AND NOT coalesce((SELECT au.is_sso_user FROM auth.users au WHERE au.id = g.user_id), false)
503+
AND NOT EXISTS (
504+
SELECT 1 FROM auth.identities ai
505+
WHERE ai.user_id = g.user_id
506+
AND ai.provider = 'sso'
507+
AND ai.provider_id = t.sso_provider_id::text
508+
)
504509
)
505510
"#,
506511
)
@@ -609,6 +614,39 @@ mod tests {
609614
);
610615
}
611616

617+
#[sqlx::test(
618+
migrations = "../../supabase/migrations",
619+
fixtures(path = "../fixtures", scripts("data_planes", "sso_tenant"))
620+
)]
621+
async fn test_snapshot_sso_enforcement(pool: sqlx::PgPool) {
622+
let mut decrypted_keys = HashMap::new();
623+
let result = try_fetch(&pool, &mut decrypted_keys)
624+
.await
625+
.expect("snapshot refresh should succeed");
626+
627+
let alice_uid: uuid::Uuid = "11111111-1111-1111-1111-111111111111".parse().unwrap();
628+
let bob_uid: uuid::Uuid = "22222222-2222-2222-2222-222222222222".parse().unwrap();
629+
let carol_uid: uuid::Uuid = "33333333-3333-3333-3333-333333333333".parse().unwrap();
630+
631+
let grants_for = |uid: uuid::Uuid| -> Vec<String> {
632+
result
633+
.user_grants
634+
.iter()
635+
.filter(move |g| g.user_id == uid)
636+
.map(|g| g.object_role.to_string())
637+
.collect()
638+
};
639+
640+
// Alice has matching SSO identity — sees both tenants.
641+
assert_eq!(grants_for(alice_uid), vec!["acmeCo/", "openCo/"]);
642+
643+
// Bob has SSO identity for a *different* provider — excluded from acmeCo.
644+
assert_eq!(grants_for(bob_uid), vec!["openCo/"]);
645+
646+
// Carol has no SSO identity — excluded from acmeCo.
647+
assert_eq!(grants_for(carol_uid), vec!["openCo/"]);
648+
}
649+
612650
#[test]
613651
fn test_task_lookups() {
614652
let snapshot = Snapshot::build_fixture(None);

supabase/migrations/00_polyfill.sql

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,23 @@ BEGIN
2727
-- Roles are cluster-wide and already exist from migrations applied to the
2828
-- primary `postgres` database. We only need to stub Supabase schemas.
2929

30-
-- Create auth schema with minimal users table stub.
30+
-- Create auth schema with minimal table stubs.
3131
create schema auth;
3232
create table auth.users (
3333
id uuid primary key,
3434
email text,
3535
is_sso_user boolean,
3636
raw_user_meta_data jsonb
3737
);
38+
create table auth.sso_providers (
39+
id uuid primary key
40+
);
41+
create table auth.identities (
42+
user_id uuid references auth.users(id),
43+
provider text,
44+
provider_id text,
45+
identity_data jsonb
46+
);
3847

3948
-- Stub for auth.uid() function.
4049
create function auth.uid() returns uuid as $uid$

0 commit comments

Comments
 (0)