diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts index 37ae7945ccc..90fdb878860 100644 --- a/src/emulator/auth/operations.ts +++ b/src/emulator/auth/operations.ts @@ -32,6 +32,7 @@ import { UsageMode, AgentProjectState, TenantProjectState, + MfaConfig, } from "./state"; import { MfaEnrollments, Schemas } from "./types"; @@ -2692,13 +2693,22 @@ function createTenant( throw new InternalError("INTERNAL_ERROR: Can only create tenant in agent project", "INTERNAL"); } + const mfaConfig = reqBody.mfaConfig ?? {}; + if (!("state" in mfaConfig)) { + mfaConfig.state = "DISABLED"; + } + if (!("enabledProviders" in mfaConfig)) { + mfaConfig.enabledProviders = []; + } + + // Default to production settings if unset const tenant = { displayName: reqBody.displayName, - allowPasswordSignup: reqBody.allowPasswordSignup, - enableEmailLinkSignin: reqBody.enableEmailLinkSignin, - enableAnonymousUser: reqBody.enableAnonymousUser, - disableAuth: reqBody.disableAuth, - mfaConfig: reqBody.mfaConfig, + allowPasswordSignup: reqBody.allowPasswordSignup ?? false, + enableEmailLinkSignin: reqBody.enableEmailLinkSignin ?? false, + enableAnonymousUser: reqBody.enableAnonymousUser ?? false, + disableAuth: reqBody.disableAuth ?? false, + mfaConfig: mfaConfig as MfaConfig, tenantId: "", // Placeholder until one is generated }; diff --git a/src/emulator/auth/state.ts b/src/emulator/auth/state.ts index 87096c77c01..c1e01cb2cb5 100644 --- a/src/emulator/auth/state.ts +++ b/src/emulator/auth/state.ts @@ -635,7 +635,22 @@ export class AgentProjectState extends ProjectState { getTenantProject(tenantId: string): TenantProjectState { if (!this.tenantProjectForTenantId.has(tenantId)) { - this.createTenantWithTenantId(tenantId, { tenantId }); + // Implicitly creates tenant if it does not already exist and sets all + // configurations to enabled. This is for convenience and differs from + // production in which configurations, are default disabled. Tests that + // need to reflect production defaults should first explicitly call + // `createTenant()` with a `Tenant` object. + this.createTenantWithTenantId(tenantId, { + tenantId, + allowPasswordSignup: true, + disableAuth: false, + mfaConfig: { + state: "ENABLED", + enabledProviders: ["PHONE_SMS"], + }, + enableAnonymousUser: true, + enableEmailLinkSignin: true, + }); } return this.tenantProjectForTenantId.get(tenantId)!; } @@ -714,42 +729,55 @@ export class TenantProjectState extends ProjectState { return this._tenantConfig; } - // TODO(lisajian): Handle divergence in tenant config settings between what is - // needed for admin SDK (default disabled, parallels production) vs emulator - // tests (default enabled, for convenience) get allowPasswordSignup() { - return this._tenantConfig.allowPasswordSignup ?? true; + return this._tenantConfig.allowPasswordSignup; } get disableAuth() { - return this._tenantConfig.disableAuth ?? false; + return this._tenantConfig.disableAuth; } get mfaConfig() { - return ( - this._tenantConfig.mfaConfig ?? { - state: "ENABLED" as const, - enabledProviders: ["PHONE_SMS" as const], - } - ); + return this._tenantConfig.mfaConfig; } get enableAnonymousUser() { - return this._tenantConfig.enableAnonymousUser ?? true; + return this._tenantConfig.enableAnonymousUser; } get enableEmailLinkSignin() { - return this._tenantConfig.enableEmailLinkSignin ?? true; + return this._tenantConfig.enableEmailLinkSignin; } delete(): void { this.parentProject.deleteTenant(this.tenantId); } - updateTenant(update: Partial, updateMask: string | undefined): Tenant { + updateTenant( + update: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], + updateMask: string | undefined + ): Tenant { // Empty masks indicate a full update if (!updateMask) { - this._tenantConfig = { ...update, tenantId: this.tenantId, name: this.tenantConfig.name }; + const mfaConfig = update.mfaConfig ?? {}; + if (!("state" in mfaConfig)) { + mfaConfig.state = "DISABLED"; + } + if (!("enabledProviders" in mfaConfig)) { + mfaConfig.enabledProviders = []; + } + + // Default to production defaults if unset + this._tenantConfig = { + tenantId: this.tenantId, + name: this.tenantConfig.name, + allowPasswordSignup: update.allowPasswordSignup ?? false, + disableAuth: update.disableAuth ?? false, + mfaConfig: mfaConfig as MfaConfig, + enableAnonymousUser: update.enableAnonymousUser ?? false, + enableEmailLinkSignin: update.enableEmailLinkSignin ?? false, + displayName: update.displayName, + }; return this.tenantConfig; } @@ -813,10 +841,17 @@ export type UserInfo = Omit< localId: string; providerUserInfo?: ProviderUserInfo[]; }; +export type MfaConfig = MakeRequired< + Schemas["GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig"], + "enabledProviders" | "state" +>; export type Tenant = Omit< - Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], - "testPhoneNumbers" -> & { tenantId: string }; + MakeRequired< + Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], + "allowPasswordSignup" | "disableAuth" | "enableAnonymousUser" | "enableEmailLinkSignin" + >, + "testPhoneNumbers" | "mfaConfig" +> & { tenantId: string; mfaConfig: MfaConfig }; interface RefreshTokenRecord { localId: string; diff --git a/src/test/emulators/auth/helpers.ts b/src/test/emulators/auth/helpers.ts index c477156ab48..1651a5c893d 100644 --- a/src/test/emulators/auth/helpers.ts +++ b/src/test/emulators/auth/helpers.ts @@ -5,7 +5,7 @@ import { expect, AssertionError } from "chai"; import { IdpJwtPayload } from "../../../emulator/auth/operations"; import { OobRecord, PhoneVerificationRecord, Tenant, UserInfo } from "../../../emulator/auth/state"; import { TestAgent, PROJECT_ID } from "./setup"; -import { MfaEnrollments } from "../../../emulator/auth/types"; +import { MfaEnrollments, Schemas } from "../../../emulator/auth/types"; export { PROJECT_ID }; export const TEST_PHONE_NUMBER = "+15555550100"; @@ -407,7 +407,7 @@ export function deleteAccount(testAgent: TestAgent, reqBody: {}): Promise + tenant?: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] ): Promise { return testAgent .post(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) diff --git a/src/test/emulators/auth/tenant.spec.ts b/src/test/emulators/auth/tenant.spec.ts index d2ef6f57665..31bab04f525 100644 --- a/src/test/emulators/auth/tenant.spec.ts +++ b/src/test/emulators/auth/tenant.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { Tenant } from "../../../emulator/auth/state"; import { expectStatusCode, registerTenant } from "./helpers"; -import { describeAuthEmulator } from "./setup"; +import { describeAuthEmulator, PROJECT_ID } from "./setup"; describeAuthEmulator("tenant management", ({ authApi }) => { describe("createTenant", () => { @@ -39,6 +39,24 @@ describeAuthEmulator("tenant management", ({ authApi }) => { expect(res.body.name).to.eql(`projects/project-id/tenants/${res.body.tenantId}`); }); }); + + it("should create a tenant with default disabled settings", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v2/projects/project-id/tenants") + .set("Authorization", "Bearer owner") + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.allowPasswordSignup).to.be.false; + expect(res.body.disableAuth).to.be.false; + expect(res.body.enableAnonymousUser).to.be.false; + expect(res.body.enableEmailLinkSignin).to.be.false; + expect(res.body.mfaConfig).to.eql({ + state: "DISABLED", + enabledProviders: [], + }); + }); + }); }); describe("getTenants", () => { @@ -55,7 +73,7 @@ describeAuthEmulator("tenant management", ({ authApi }) => { }); }); - it("should create tenants if they do not exist", async () => { + it("should create tenants with default enabled settings if they do not exist", async () => { // No projects exist initially const projectId = "project-id"; await authApi() @@ -71,6 +89,14 @@ describeAuthEmulator("tenant management", ({ authApi }) => { const createdTenant: Tenant = { tenantId, name: `projects/${projectId}/tenants/${tenantId}`, + allowPasswordSignup: true, + disableAuth: false, + enableAnonymousUser: true, + enableEmailLinkSignin: true, + mfaConfig: { + enabledProviders: ["PHONE_SMS"], + state: "ENABLED", + }, }; await authApi() .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenantId}`) @@ -325,5 +351,28 @@ describeAuthEmulator("tenant management", ({ authApi }) => { }); }); }); + + it("performs a full update with production defaults if the update mask is empty", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + + await authApi() + .patch( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}` + ) + .set("Authorization", "Bearer owner") + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.allowPasswordSignup).to.be.false; + expect(res.body.disableAuth).to.be.false; + expect(res.body.enableAnonymousUser).to.be.false; + expect(res.body.enableEmailLinkSignin).to.be.false; + expect(res.body.mfaConfig).to.eql({ + enabledProviders: [], + state: "DISABLED", + }); + }); + }); }); });