diff --git a/cli/package-lock.json b/cli/package-lock.json index 8b469f6a2..471eabca4 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yc-software/qm", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yc-software/qm", - "version": "0.1.6", + "version": "0.1.7", "license": "MIT", "bin": { "qm": "dist/bin/qm.js" diff --git a/cli/package.json b/cli/package.json index 4fbc357d4..588271b14 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@yc-software/qm", - "version": "0.1.6", + "version": "0.1.7", "license": "MIT", "description": "Control-plane CLI for portable QM deployments on Docker, Fly, and AWS.", "type": "module", diff --git a/cli/src/secrets.ts b/cli/src/secrets.ts index ed061840b..8faea9b94 100644 --- a/cli/src/secrets.ts +++ b/cli/src/secrets.ts @@ -316,7 +316,10 @@ export const FIRST_PARTY_SECRET_SPECS: readonly SecretSpec[] = [ { name: "AUTH_ALLOWED_EMAILS", service: "auth", - required: { when: { kind: "env-absent", service: "auth", name: "AUTH_ALLOWED_EMAIL_DOMAIN" } }, + required: { + when: { kind: "env-absent", service: "auth", name: "AUTH_ALLOWED_EMAIL_DOMAIN" }, + optionalOtherwise: true, + }, description: "Comma-separated email addresses allowed to sign in through the built-in broker.", }, { @@ -331,6 +334,7 @@ export const FIRST_PARTY_SECRET_SPECS: readonly SecretSpec[] = [ { kind: "env-absent", service: "auth", name: "AUTH_ALLOWED_EMAIL_DOMAIN" }, ], }, + optionalOtherwise: true, }, description: "Email addresses allowed to sign in; the portal enforces the same list the broker does.", }, diff --git a/cli/test/auth-broker.test.ts b/cli/test/auth-broker.test.ts index f7711ccc7..62806014e 100644 --- a/cli/test/auth-broker.test.ts +++ b/cli/test/auth-broker.test.ts @@ -137,7 +137,10 @@ test("the broker's generated secrets reach both sides under the right names", () assert.ok(!names.has("OIDC_CLIENT_SECRET"), "the operator is never asked for an OIDC client secret in broker mode"); assert.ok(!names.has("OIDC_CLIENT_ID")); assert.ok(!names.has("PORTAL_EXPECTED_TEAM_ID")); - assert.ok(!names.has("AUTH_ALLOWED_EMAILS"), "a configured domain removes the per-address allowlist requirement"); + const allowed = secrets.find((secret) => secret.name === "AUTH_ALLOWED_EMAILS")!; + assert.equal(allowed.required, false); + assert.deepEqual(runtimeSecretNames("auth", allowed), ["AUTH_ALLOWED_EMAILS"]); + assert.deepEqual(runtimeSecretNames("portal", allowed), ["OIDC_ALLOWED_EMAILS"]); assert.ok(names.has("RESEND_API_KEY")); assert.ok(!names.has("SMTP_HOST"), "only the configured transport's credentials are collected"); assert.ok(secretsForService(config, "auth").some((secret) => secret.name === "CORE_SIGNING_SECRET")); diff --git a/plugins/portal/src/oidc.ts b/plugins/portal/src/oidc.ts index 7cd70dd0d..ce86ceb83 100644 --- a/plugins/portal/src/oidc.ts +++ b/plugins/portal/src/oidc.ts @@ -135,19 +135,16 @@ export function resolvePrincipal( const verified = args.userinfo.email_verified; if (verified !== true && verified !== "true") throw new Error("email is not verified by the identity provider"); const email = rawEmail.trim().toLowerCase(); - if ( - rule.allowedEmails?.length && - !rule.allowedEmails.map((allowed) => allowed.trim().toLowerCase()).includes(email) - ) { - throw new Error("account is not on the permitted email list"); - } + if (rule.allowedEmails?.map((allowed) => allowed.trim().toLowerCase()).includes(email)) return email; if (rule.allowedEmailDomain) { const domain = rule.allowedEmailDomain.toLowerCase(); if (!email.endsWith(`@${domain}`)) throw new Error("account is outside the permitted domain"); const hd = args.userinfo.hd ?? args.claims.hd; if (typeof hd === "string" && hd.toLowerCase() !== domain) throw new Error("account is outside the permitted domain"); + return email; } + if (rule.allowedEmails?.length) throw new Error("account is not on the permitted email list"); return email; } diff --git a/plugins/portal/test/oidc.test.ts b/plugins/portal/test/oidc.test.ts index 9dc6939de..0a19c279b 100644 --- a/plugins/portal/test/oidc.test.ts +++ b/plugins/portal/test/oidc.test.ts @@ -232,3 +232,19 @@ test("resolvePrincipal allowedEmails permits only the seeded verified addresses" /permitted email list/, ); }); + +test("resolvePrincipal permits an email matching either the list or domain", () => { + const rule = { + claim: "email" as const, + allowedEmails: ["admin@gmail.com"], + allowedEmailDomain: "example.com", + }; + for (const email of ["admin@gmail.com", "member@example.com"]) { + assert.equal(resolvePrincipal(rule, { sub: "g", claims: {}, userinfo: { email, email_verified: true } }), email); + } + assert.throws( + () => + resolvePrincipal(rule, { sub: "g", claims: {}, userinfo: { email: "other@gmail.com", email_verified: true } }), + /permitted domain/, + ); +});