Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 51 additions & 10 deletions .claude/skills/setup-mocksaml/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
name: setup-mocksaml
description: Set up MockSAML as a local SAML IdP for testing SSO flows (grant migration, invite enforcement). Run when setting up a fresh local env or after `supabase db reset`.
disable-model-invocation: true
allowed-tools: Bash(*), Read
---

# Set Up MockSAML for Local SSO Testing
Expand Down Expand Up @@ -50,6 +49,18 @@ If `GOTRUE_SAML_ENABLED=true` is already set, skip to step 6.
openssl genrsa 2048 > /tmp/saml_key.pem
```

GoTrue requires PKCS#1 format. Check the header of the generated key:

```bash
head -1 /tmp/saml_key.pem
```

- `BEGIN RSA PRIVATE KEY` → PKCS#1, good to go.
- `BEGIN PRIVATE KEY` → PKCS#8 (OpenSSL 3.x default). Convert it:
```bash
openssl rsa -traditional -in /tmp/saml_key.pem -out /tmp/saml_key.pem
```

Strip to raw base64 (no PEM headers, no newlines):

```bash
Expand All @@ -67,10 +78,28 @@ Capture the current container's env vars, image, and network:
<docker-prefix> docker inspect supabase_auth_flow --format '{{json .NetworkSettings.Networks}}'
```

Stop and remove the old container, then recreate with all original env vars
plus the SAML vars. **Important:** override `API_EXTERNAL_URL` to include the
`/auth/v1` prefix — GoTrue uses this to generate the SAML ACS callback URL,
and without the prefix Kong won't route the callback correctly.
Build an env file for the new container. Using `--env-file` avoids shell
parsing issues with values that contain template syntax (e.g.
`GOTRUE_SMS_TEMPLATE=Your code is {{ .Code }}`).

```bash
grep -v -E '^(PATH=|API_EXTERNAL_URL=)' /tmp/auth_env.txt > /tmp/auth_env_filtered.txt
echo "GOTRUE_SAML_ENABLED=true" >> /tmp/auth_env_filtered.txt
echo "GOTRUE_SAML_PRIVATE_KEY=$SAML_KEY_B64" >> /tmp/auth_env_filtered.txt
echo "API_EXTERNAL_URL=http://127.0.0.1:5431/auth/v1" >> /tmp/auth_env_filtered.txt
```

**Important:** the `API_EXTERNAL_URL` override includes the `/auth/v1` prefix —
GoTrue uses this to generate the SAML ACS callback URL, and without the prefix
Kong won't route the callback correctly.

If using Lima, copy the env file into the VM before running docker:

```bash
limactl copy /tmp/auth_env_filtered.txt <vm>:/tmp/auth_env_filtered.txt
```

Stop and remove the old container, then recreate:

```bash
<docker-prefix> docker stop supabase_auth_flow && <docker-prefix> docker rm supabase_auth_flow
Expand All @@ -79,10 +108,7 @@ and without the prefix Kong won't route the callback correctly.
--name supabase_auth_flow \
--network <network-name> \
--restart always \
-e GOTRUE_SAML_ENABLED=true \
-e GOTRUE_SAML_PRIVATE_KEY=$SAML_KEY_B64 \
-e API_EXTERNAL_URL=http://127.0.0.1:5431/auth/v1 \
<all original -e flags from /tmp/auth_env.txt, excluding PATH= and API_EXTERNAL_URL=> \
--env-file /tmp/auth_env_filtered.txt \
<image> auth
```

Expand All @@ -102,6 +128,15 @@ supabase status --output json

Extract `SERVICE_ROLE_KEY` from the output.

If `supabase status` fails (e.g. Docker runs inside a Lima VM), read the key
from Kong's config instead — it's always accessible since Kong handles routing:

```bash
<docker-prefix> docker exec supabase_kong_flow cat /home/kong/kong.yml
```

Look for the `service_role` JWT in the authorization header rewriting rules.

### 7. Check if MockSAML is already registered

```bash
Expand All @@ -114,14 +149,20 @@ register a new one. If reusing, skip to step 9.

### 8. Register MockSAML as an SSO provider

Ask the user which email domain to associate with the SSO provider (default:
`example.com`). This controls which email addresses are routed through SAML
login. MockSAML's default test user is `jackson@example.com`, so `example.com`
works out of the box — but the user may want a different domain to match their
test data.

```bash
curl -X POST 'http://127.0.0.1:5431/auth/v1/admin/sso/providers' \
-H 'Authorization: Bearer <SERVICE_ROLE_KEY>' \
-H 'Content-Type: application/json' \
-d '{
"type": "saml",
"metadata_url": "https://mocksaml.com/api/saml/metadata",
"domains": ["example.com"]
"domains": ["<DOMAIN>"]
}'
```

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions crates/control-plane-api/src/fixtures/sso_tenant.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
do $$
declare
data_plane_one_id flowid := '111111111111';

alice_uid uuid := '11111111-1111-1111-1111-111111111111';
bob_uid uuid := '22222222-2222-2222-2222-222222222222';
carol_uid uuid := '33333333-3333-3333-3333-333333333333';

acme_provider_id uuid := 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
other_provider_id uuid := 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';

last_pub_id flowid := '000000000002';

begin

-- SSO providers
insert into auth.sso_providers (id) values
(acme_provider_id),
(other_provider_id)
;

-- Users
insert into auth.users (id, email) values
(alice_uid, 'alice@acme.co'),
(bob_uid, 'bob@other.co'),
(carol_uid, 'carol@example.com')
;

-- Alice has an SSO identity matching acme's provider.
-- GoTrue stores SSO identities with provider = 'sso:<provider_id>'.
insert into auth.identities (user_id, provider, provider_id) values
(alice_uid, 'sso:' || acme_provider_id::text, acme_provider_id::text)
;

-- Bob has an SSO identity, but for a different provider.
insert into auth.identities (user_id, provider, provider_id) values
(bob_uid, 'sso:' || other_provider_id::text, other_provider_id::text)
;

-- Carol has no SSO identity (social login only).

-- Tenants: acmeCo/ with SSO, openCo/ without SSO.
insert into public.tenants (id, tenant, sso_provider_id) values
(internal.id_generator(), 'acmeCo/', acme_provider_id),
(internal.id_generator(), 'openCo/', null)
;

-- Alice is admin on acmeCo/ and openCo/.
insert into public.user_grants (user_id, object_role, capability) values
(alice_uid, 'acmeCo/', 'admin'),
(alice_uid, 'openCo/', 'admin')
;

-- Give alice read on data planes so specs can resolve.
insert into public.role_grants (subject_role, object_role, capability) values
('acmeCo/', 'ops/dp/public/', 'read')
;

end
$$;
6 changes: 1 addition & 5 deletions crates/control-plane-api/src/publications/quotas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,7 @@ fn get_deltas(built: &build::Output) -> BTreeMap<&str, (i32, i32)> {
}

fn tenant(name: &impl AsRef<str>) -> &str {
let idx = name
.as_ref()
.find('/')
.expect("catalog name must contain at least one /");
name.as_ref().split_at(idx + 1).0
models::tenant_from(name.as_ref())
}

pub async fn check_resource_quotas(
Expand Down
Loading
Loading