diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 2986869ef2..c7090af304 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -23,10 +23,35 @@ export interface ActionCodeSettings { url: string; } +// @public +export interface AllowByDefault { + disallowedRegions: string[]; +} + +// @public +export interface AllowByDefaultWrap { + allowByDefault: AllowByDefault; + // @alpha (undocumented) + allowlistOnly?: never; +} + +// @public +export interface AllowlistOnly { + allowedRegions: string[]; +} + +// @public +export interface AllowlistOnlyWrap { + // @alpha (undocumented) + allowByDefault?: never; + allowlistOnly: AllowlistOnly; +} + // @public export class Auth extends BaseAuth { // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts get app(): App; + projectConfigManager(): ProjectConfigManager; tenantManager(): TenantManager; } @@ -309,6 +334,18 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { toJSON(): object; } +// @public +export class ProjectConfig { + readonly smsRegionConfig?: SmsRegionConfig; + toJSON(): object; +} + +// @public +export class ProjectConfigManager { + getProjectConfig(): Promise; + updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise; +} + // @public export interface ProviderIdentifier { // (undocumented) @@ -342,6 +379,9 @@ export interface SessionCookieOptions { expiresIn: number; } +// @public +export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; + // @public export class Tenant { // (undocumented) @@ -349,6 +389,7 @@ export class Tenant { readonly displayName?: string; get emailSignInConfig(): EmailSignInProviderConfig | undefined; get multiFactorConfig(): MultiFactorConfig | undefined; + readonly smsRegionConfig?: SmsRegionConfig; readonly tenantId: string; readonly testPhoneNumbers?: { [phoneNumber: string]: string; @@ -391,6 +432,11 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor phoneNumber: string; } +// @public +export interface UpdateProjectConfigRequest { + smsRegionConfig?: SmsRegionConfig; +} + // @public export interface UpdateRequest { disabled?: boolean; @@ -411,6 +457,7 @@ export interface UpdateTenantRequest { displayName?: string; emailSignInConfig?: EmailSignInProviderConfig; multiFactorConfig?: MultiFactorConfig; + smsRegionConfig?: SmsRegionConfig; testPhoneNumbers?: { [phoneNumber: string]: string; } | null; diff --git a/package-lock.json b/package-lock.json index 24d1405289..b7c5fd9a87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.0.1", + "version": "11.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -407,35 +407,77 @@ } }, "@firebase/app": { - "version": "0.7.33", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.33.tgz", - "integrity": "sha512-7K7ljuFhbT9uF0gTvuA7ZrpFFnS1eJLplfjJdjDQFWyjD6Cwk0FXNdu75WvoWgywoQCGiVBX8u5Jb437UQIhWQ==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.8.2.tgz", + "integrity": "sha512-ByNDCe8h9O/szO3XVTrS484MtqBOKriVaNCQC7Y7KgZSaiA0OOWmIY5vwi63mBTYetqMNN5VGiG/6ZSmGIZyoQ==", "dev": true, "requires": { - "@firebase/component": "0.5.17", + "@firebase/component": "0.5.20", "@firebase/logger": "0.3.3", - "@firebase/util": "1.6.3", + "@firebase/util": "1.7.2", "idb": "7.0.1", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.20.tgz", + "integrity": "sha512-wP51tQBlPFprfAWxWjzC/56hG4APhl43jFsgwuqCl3bhVbiKcr278QbrbGNmIXDeGKo4sGZLAnH9whl2apeCmA==", + "dev": true, + "requires": { + "@firebase/util": "1.7.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.2.tgz", + "integrity": "sha512-P3aTihYEMoz2QQlcn0T7av7HLEK9gsTc1ZiN9VA8wnUtEJscUNemCmTmP3RRysqEb3Z+tVVoycztY8f6R36rRw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/app-compat": { - "version": "0.1.34", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.34.tgz", - "integrity": "sha512-3XSrHDgtASIH8j6sDngiKykDcqlEM0mYplJTYdyN69ruZ1o0M+bUhIvX9mUoRelWZGT1BcMpFmh/62vz/wN72Q==", + "version": "0.1.37", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.37.tgz", + "integrity": "sha512-doTKYGlVc8ZiQNOl66rpkU/YItRyOxCgMp4YWThXkPM4T/pTi4a9IMCe8K88gVNeYWd8sKW4vSnxjcOG5hQXEA==", "dev": true, "requires": { - "@firebase/app": "0.7.33", - "@firebase/component": "0.5.17", + "@firebase/app": "0.8.2", + "@firebase/component": "0.5.20", "@firebase/logger": "0.3.3", - "@firebase/util": "1.6.3", + "@firebase/util": "1.7.2", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.20.tgz", + "integrity": "sha512-wP51tQBlPFprfAWxWjzC/56hG4APhl43jFsgwuqCl3bhVbiKcr278QbrbGNmIXDeGKo4sGZLAnH9whl2apeCmA==", + "dev": true, + "requires": { + "@firebase/util": "1.7.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.2.tgz", + "integrity": "sha512-P3aTihYEMoz2QQlcn0T7av7HLEK9gsTc1ZiN9VA8wnUtEJscUNemCmTmP3RRysqEb3Z+tVVoycztY8f6R36rRw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/app-types": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.7.0.tgz", - "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.8.0.tgz", + "integrity": "sha512-Lec3VVquUwXPn2UReGSsfTxuMBVRmzGIwA/CJnF0LQuPgv9kOmXk9mVqsDMfHxHtqjai0n6wWHR2TqjdVV/bYA==" }, "@firebase/auth": { "version": "0.20.7", @@ -481,44 +523,93 @@ "version": "0.5.17", "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.17.tgz", "integrity": "sha512-mTM5CBSIlmI+i76qU4+DhuExnWtzcPS3cVgObA3VAjliPPr3GrUlTaaa8KBGfxsD27juQxMsYA0TvCR5X+GQ3Q==", + "dev": true, "requires": { "@firebase/util": "1.6.3", "tslib": "^2.1.0" } }, "@firebase/database": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.6.tgz", - "integrity": "sha512-5IZIBw2LT50Z8mwmKYmdX37p+Gg2HgeJsrruZmRyOSVgbfoY4Pg87n1uFx6qWqDmfL6HwQgwcrrQfVIXE3C5SA==", + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.13.9.tgz", + "integrity": "sha512-raQEBgQQybaEoMloJL8wWHQywGQ9mF2VbitvHydsbSNn+KL/xRDjXeQZPuuSbRjkYV6mR8jvQB7gpnzQQNE8Qg==", "requires": { "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.17", + "@firebase/component": "0.5.20", "@firebase/logger": "0.3.3", - "@firebase/util": "1.6.3", + "@firebase/util": "1.7.2", "faye-websocket": "0.11.4", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.20.tgz", + "integrity": "sha512-wP51tQBlPFprfAWxWjzC/56hG4APhl43jFsgwuqCl3bhVbiKcr278QbrbGNmIXDeGKo4sGZLAnH9whl2apeCmA==", + "requires": { + "@firebase/util": "1.7.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.2.tgz", + "integrity": "sha512-P3aTihYEMoz2QQlcn0T7av7HLEK9gsTc1ZiN9VA8wnUtEJscUNemCmTmP3RRysqEb3Z+tVVoycztY8f6R36rRw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/database-compat": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.6.tgz", - "integrity": "sha512-Ls1BAODaiDYgeJljrIgSuC7JkFIY/HNhhNYebzZSoGQU62RuvnaO3Qgp2EH6h2LzHyRnycNadfh1suROtPaUIA==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.2.9.tgz", + "integrity": "sha512-zzyFM3+jW/qYtHojiQirHXGXYyElbqVngEEn/i2gXoSzcK0Y2AL5oHAqGYXLaaW0+t4Zwnssh3HnQJM8C1D0fw==", "requires": { - "@firebase/component": "0.5.17", - "@firebase/database": "0.13.6", - "@firebase/database-types": "0.9.13", + "@firebase/component": "0.5.20", + "@firebase/database": "0.13.9", + "@firebase/database-types": "0.9.16", "@firebase/logger": "0.3.3", - "@firebase/util": "1.6.3", + "@firebase/util": "1.7.2", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.20.tgz", + "integrity": "sha512-wP51tQBlPFprfAWxWjzC/56hG4APhl43jFsgwuqCl3bhVbiKcr278QbrbGNmIXDeGKo4sGZLAnH9whl2apeCmA==", + "requires": { + "@firebase/util": "1.7.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.2.tgz", + "integrity": "sha512-P3aTihYEMoz2QQlcn0T7av7HLEK9gsTc1ZiN9VA8wnUtEJscUNemCmTmP3RRysqEb3Z+tVVoycztY8f6R36rRw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/database-types": { - "version": "0.9.13", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.13.tgz", - "integrity": "sha512-dIJ1zGe3EHMhwcvukTOPzYlFYFIG1Et5Znl7s7y/ZTN2/toARRNnsv1qCKvqevIMYKvIrRsYOYfOXDS8l1YIJA==", + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.16.tgz", + "integrity": "sha512-dK/uFgHisrVijSoHf9RLJ7NwvlOul2rO/z9ufOSbGd8/TqFVASXz+19mynhDIoSEnyQtJC/NTyBzSPfjz0w61w==", "requires": { - "@firebase/app-types": "0.7.0", - "@firebase/util": "1.6.3" + "@firebase/app-types": "0.8.0", + "@firebase/util": "1.7.2" + }, + "dependencies": { + "@firebase/util": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.7.2.tgz", + "integrity": "sha512-P3aTihYEMoz2QQlcn0T7av7HLEK9gsTc1ZiN9VA8wnUtEJscUNemCmTmP3RRysqEb3Z+tVVoycztY8f6R36rRw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/logger": { @@ -533,14 +624,15 @@ "version": "1.6.3", "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.6.3.tgz", "integrity": "sha512-FujteO6Zjv6v8A4HS+t7c+PjU0Kaxj+rOnka0BsI/twUaCC9t8EQPmXpWZdk7XfszfahJn2pqsflUWUhtUkRlg==", + "dev": true, "requires": { "tslib": "^2.1.0" } }, "@google-cloud/firestore": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.3.0.tgz", - "integrity": "sha512-EtEOl1A8lDFv+t2X/GyyvCDSWamUfBPVep0y0qHvO7CKD4MH/4MiPKe/+kcq1h8rynX6Ths/E/jtfAiizFCVrA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.4.0.tgz", + "integrity": "sha512-qhL5V8S6uIGlESQYC/TMKISlKHaM2qSACz0X15ID0s4F1NuVgSM3Z2FS10WYHdCGIwJ2C73xdLaS+ByFDsu7sg==", "optional": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -606,9 +698,9 @@ } }, "@grpc/grpc-js": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.1.tgz", - "integrity": "sha512-GVtMU4oh/TeKkWGzXUEsyZtyvSUIT1z49RtGH1UnEGeL+sLuxKl8QH3KZTlSB329R1sWJmesm5hQ5CxXdYH9dg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.2.tgz", + "integrity": "sha512-MqqbVynbe3VUSnApFW/dpkDaa9T1ASqRnMWeSPGFO/Ro98R7XUDLacfeBa7RaSI1iFu9GYk5gBKARf0zipFe4w==", "optional": true, "requires": { "@grpc/proto-loader": "^0.7.0", @@ -840,23 +932,23 @@ } }, "@microsoft/api-extractor": { - "version": "7.31.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.31.2.tgz", - "integrity": "sha512-ZODCU9ckTS9brXiZpUW2iDrnAg7jLxeLBM1AkPpSZFcbG/8HGLvfKOKrd71VIJHjc52x2lB8xj7ZWksnP7AOBA==", + "version": "7.33.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.33.4.tgz", + "integrity": "sha512-uZG4CHxVcQNpXBC77GwHaKFwGI9vAnzORY4fFN5JuTnQQDKS0vi4BazP4pmYYwbb8IdH4ocQSwOA3j9Ul/sWmg==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.24.2", + "@microsoft/api-extractor-model": "7.25.1", "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.52.0", - "@rushstack/rig-package": "0.3.15", - "@rushstack/ts-command-line": "4.12.3", + "@rushstack/node-core-library": "3.53.2", + "@rushstack/rig-package": "0.3.17", + "@rushstack/ts-command-line": "4.13.0", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", "semver": "~7.3.0", "source-map": "~0.6.1", - "typescript": "~4.7.4" + "typescript": "~4.8.4" }, "dependencies": { "@microsoft/tsdoc": { @@ -866,9 +958,9 @@ "dev": true }, "@rushstack/node-core-library": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.52.0.tgz", - "integrity": "sha512-Z+MAP//G3rEGZd3JxJcBGcPYJlh8pvPoLMTLa5Sy6FTE6hRPzN+5J8DT7BbTmlqZaL6SZpXF30heRUbnYOvujw==", + "version": "3.53.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.53.2.tgz", + "integrity": "sha512-FggLe5DQs0X9MNFeJN3/EXwb+8hyZUTEp2i+V1e8r4Va4JgkjBNY0BuEaQI+3DW6S4apV3UtXU3im17MSY00DA==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -882,9 +974,9 @@ } }, "@rushstack/ts-command-line": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.12.3.tgz", - "integrity": "sha512-Pdij22RotMXzI+HWHyYCvw0RMZhiP5a6Za/96XamZ1+mxmpSm4ujf8TROKxGAHySmR5A8iNVSlzhNMnUlFQE6g==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.13.0.tgz", + "integrity": "sha512-crLT31kl+qilz0eBRjqqYO06CqwbElc0EvzS6jI69B9Ikt1SkkSzIZ2iDP7zt/rd1ZYipKIS9hf9CQR9swDIKg==", "dev": true, "requires": { "@types/argparse": "1.0.38", @@ -922,24 +1014,18 @@ "requires": { "path-parse": "^1.0.6" } - }, - "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "dev": true } } }, "@microsoft/api-extractor-model": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.24.2.tgz", - "integrity": "sha512-uUvjqTCY7hYERWGks+joTioN1QYHIucCDy7I/JqLxFxLbFXE5dpc1X7L+FG4PN/s8QYL24DKt0fqJkgcrFKLTw==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.25.1.tgz", + "integrity": "sha512-AaZ0ohCGLRjWiZviM+0p/DaxgMhbawS183LW2+CSqyEBh6wZks7NjoyhzhibAYapS4omnrmv96+0V/2wBvnIZQ==", "dev": true, "requires": { "@microsoft/tsdoc": "0.14.1", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.52.0" + "@rushstack/node-core-library": "3.53.2" }, "dependencies": { "@microsoft/tsdoc": { @@ -949,9 +1035,9 @@ "dev": true }, "@rushstack/node-core-library": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.52.0.tgz", - "integrity": "sha512-Z+MAP//G3rEGZd3JxJcBGcPYJlh8pvPoLMTLa5Sy6FTE6hRPzN+5J8DT7BbTmlqZaL6SZpXF30heRUbnYOvujw==", + "version": "3.53.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.53.2.tgz", + "integrity": "sha512-FggLe5DQs0X9MNFeJN3/EXwb+8hyZUTEp2i+V1e8r4Va4JgkjBNY0BuEaQI+3DW6S4apV3UtXU3im17MSY00DA==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -1159,9 +1245,9 @@ } }, "@rushstack/rig-package": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.15.tgz", - "integrity": "sha512-jxVfvO5OnkRlYRhcVDZWvwiI2l4pv37HDJRtyg5HbD8Z/I8Xj32RICgrxS5xMeGGytobrg5S6OfPOHskg7Nw+A==", + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.17.tgz", + "integrity": "sha512-nxvAGeIMnHl1LlZSQmacgcRV4y1EYtgcDIrw6KkeVjudOMonlxO482PhDj3LVZEp6L7emSf6YSO2s5JkHlwfZA==", "dev": true, "requires": { "resolve": "~1.17.0", @@ -1426,9 +1512,9 @@ "dev": true }, "@types/mocha": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", - "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg==", "dev": true }, "@types/nock": { @@ -1477,6 +1563,12 @@ "@types/request": "*" } }, + "@types/semver": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz", + "integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==", + "dev": true + }, "@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", @@ -1524,14 +1616,14 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", - "integrity": "sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.1.tgz", + "integrity": "sha512-FsWboKkWdytGiXT5O1/R9j37YgcjO8MKHSUmWnIEjVaz0krHkplPnYi7mwdb+5+cs0toFNQb0HIrN7zONdIEWg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.38.1", - "@typescript-eslint/type-utils": "5.38.1", - "@typescript-eslint/utils": "5.38.1", + "@typescript-eslint/scope-manager": "5.40.1", + "@typescript-eslint/type-utils": "5.40.1", + "@typescript-eslint/utils": "5.40.1", "debug": "^4.3.4", "ignore": "^5.2.0", "regexpp": "^3.2.0", @@ -1540,53 +1632,96 @@ } }, "@typescript-eslint/parser": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.1.tgz", - "integrity": "sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw==", + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz", + "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.38.1", - "@typescript-eslint/types": "5.38.1", - "@typescript-eslint/typescript-estree": "5.38.1", + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/typescript-estree": "5.40.0", "debug": "^4.3.4" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", + "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0" + } + }, + "@typescript-eslint/types": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", + "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", + "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", + "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.40.0", + "eslint-visitor-keys": "^3.3.0" + } + } } }, "@typescript-eslint/scope-manager": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz", - "integrity": "sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.1.tgz", + "integrity": "sha512-jkn4xsJiUQucI16OLCXrLRXDZ3afKhOIqXs4R3O+M00hdQLKR58WuyXPZZjhKLFCEP2g+TXdBRtLQ33UfAdRUg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.38.1", - "@typescript-eslint/visitor-keys": "5.38.1" + "@typescript-eslint/types": "5.40.1", + "@typescript-eslint/visitor-keys": "5.40.1" } }, "@typescript-eslint/type-utils": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz", - "integrity": "sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.1.tgz", + "integrity": "sha512-DLAs+AHQOe6n5LRraXiv27IYPhleF0ldEmx6yBqBgBLaNRKTkffhV1RPsjoJBhVup2zHxfaRtan8/YRBgYhU9Q==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.38.1", - "@typescript-eslint/utils": "5.38.1", + "@typescript-eslint/typescript-estree": "5.40.1", + "@typescript-eslint/utils": "5.40.1", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.1.tgz", - "integrity": "sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.1.tgz", + "integrity": "sha512-Icg9kiuVJSwdzSQvtdGspOlWNjVDnF3qVIKXdJ103o36yRprdl3Ge5cABQx+csx960nuMF21v8qvO31v9t3OHw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz", - "integrity": "sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.1.tgz", + "integrity": "sha512-5QTP/nW5+60jBcEPfXy/EZL01qrl9GZtbgDZtDPlfW5zj/zjNrdI2B5zMUHmOsfvOr2cWqwVdWjobCiHcedmQA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.38.1", - "@typescript-eslint/visitor-keys": "5.38.1", + "@typescript-eslint/types": "5.40.1", + "@typescript-eslint/visitor-keys": "5.40.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1595,35 +1730,31 @@ } }, "@typescript-eslint/utils": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.1.tgz", - "integrity": "sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.1.tgz", + "integrity": "sha512-a2TAVScoX9fjryNrW6BZRnreDUszxqm9eQ9Esv8n5nXApMW0zeANUYlwh/DED04SC/ifuBvXgZpIK5xeJHQ3aw==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.38.1", - "@typescript-eslint/types": "5.38.1", - "@typescript-eslint/typescript-estree": "5.38.1", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.40.1", + "@typescript-eslint/types": "5.40.1", + "@typescript-eslint/typescript-estree": "5.40.1", "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz", - "integrity": "sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA==", + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.1.tgz", + "integrity": "sha512-A2DGmeZ+FMja0geX5rww+DpvILpwo1OsiQs0M+joPWJYsiEFBLsH0y1oFymPNul6Z5okSmHpP4ivkc2N0Cgfkw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/types": "5.40.1", "eslint-visitor-keys": "^3.3.0" } }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2284,13 +2415,13 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "bcrypt": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", - "integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", "dev": true, "requires": { - "@mapbox/node-pre-gyp": "^1.0.0", - "node-addon-api": "^3.1.0" + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" } }, "bcrypt-pbkdf": { @@ -5897,14 +6028,14 @@ } }, "jwks-rsa": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.4.tgz", - "integrity": "sha512-mpArfgPkUpX11lNtGxsF/szkasUcbWHGplZl/uFvFO2NuMHmt0dQXIihh0rkPU2yQd5niQtuUHbXnG/WKiXF6Q==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.5.tgz", + "integrity": "sha512-IODtn1SwEm7n6GQZnQLY0oxKDrMh7n/jRH1MzE8mlxWMrh2NnMyOsXTebu8vJ1qCpmuTJcL4DdiE0E4h8jnwsA==", "requires": { - "@types/express": "^4.17.13", - "@types/jsonwebtoken": "^8.5.8", + "@types/express": "^4.17.14", + "@types/jsonwebtoken": "^8.5.9", "debug": "^4.3.4", - "jose": "^2.0.5", + "jose": "^2.0.6", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" }, @@ -6315,9 +6446,9 @@ "optional": true }, "marked": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.1.0.tgz", - "integrity": "sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.1.1.tgz", + "integrity": "sha512-0cNMnTcUJPxbA6uWmCmjWz4NJRe/0Xfk2NhXCUHjew9qJzFN20krFnsUe7QynwqOwa5m1fZ4UDg0ycKFVC0ccw==", "optional": true }, "matchdep": { @@ -6515,9 +6646,9 @@ } }, "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, "minipass": { "version": "3.3.4", @@ -6565,12 +6696,11 @@ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, "mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", + "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", "dev": true, "requires": { - "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", @@ -6888,9 +7018,9 @@ } }, "node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==", "dev": true }, "node-fetch": { @@ -8817,9 +8947,9 @@ "dev": true }, "sinon": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.0.tgz", - "integrity": "sha512-ugA6BFmE+WrJdh0owRZHToLd32Uw3Lxq6E6LtNRU+xTVBefx632h03Q7apXWRsRdZAJ41LB8aUfn2+O4jsDNMw==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.1.tgz", + "integrity": "sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.8.3", @@ -9648,9 +9778,9 @@ "optional": true }, "uglify-js": { - "version": "3.17.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.2.tgz", - "integrity": "sha512-bbxglRjsGQMchfvXZNusUcYgiB9Hx2K4AHYXQy2DITZ9Rd+JzhX7+hoocE5Winr7z2oHvPsekkBwXtigvxevXg==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.3.tgz", + "integrity": "sha512-JmMFDME3iufZnBpyKL+uS78LRiC+mK55zWfM5f/pWBJfpOttXAqYfdDGRukYhJuyRinvPVAtUhvy7rlDybNtFg==", "optional": true }, "unbox-primitive": { @@ -10110,12 +10240,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.5.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", - "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", "dev": true, "requires": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", @@ -10124,6 +10254,17 @@ "yargs-parser": "^21.0.0" }, "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/package.json b/package.json index a89394b1bf..faab4fb1b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.0.1", + "version": "11.2.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -198,7 +198,7 @@ "uuid": "^9.0.0" }, "optionalDependencies": { - "@google-cloud/firestore": "^6.3.0", + "@google-cloud/firestore": "^6.4.0", "@google-cloud/storage": "^6.5.2" }, "devDependencies": { @@ -214,7 +214,7 @@ "@types/jsonwebtoken": "8.5.1", "@types/lodash": "^4.14.104", "@types/minimist": "^1.2.2", - "@types/mocha": "^9.0.0", + "@types/mocha": "^10.0.0", "@types/nock": "^11.1.0", "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index a962a4f719..2893d49a9d 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -42,6 +42,7 @@ import { OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest } from './auth-config'; +import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -102,7 +103,6 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace( 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); - /** Maximum allowed number of tenants to download at one time. */ const MAX_LIST_TENANT_PAGE_SIZE = 1000; @@ -1981,6 +1981,29 @@ export abstract class AbstractAuthRequestHandler { } } +/** Instantiates the getConfig endpoint settings. */ +const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the updateConfig endpoint settings. */ +const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update project config', + ); + } + }); /** Instantiates the getTenant endpoint settings. */ const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET') @@ -2049,13 +2072,13 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') /** - * Utility for sending requests to Auth server that are Auth instance related. This includes user and - * tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines + * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant, + * and project config management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines * additional tenant management related APIs. */ export class AuthRequestHandler extends AbstractAuthRequestHandler { - protected readonly tenantMgmtResourceBuilder: AuthResourceUrlBuilder; + protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder; /** * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. @@ -2065,7 +2088,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: App) { super(app); - this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2'); + this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2'); } /** @@ -2082,6 +2105,35 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { return new AuthResourceUrlBuilder(this.app, 'v2'); } + /** + * Get the current project's config + * @returns A promise that resolves with the project config information. + */ + public getProjectConfig(): Promise { + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_CONFIG, {}, {}) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } + + /** + * Update the current project's config. + * @returns A promise that resolves with the project config information. + */ + public updateProjectConfig(options: UpdateProjectConfigRequest): Promise { + try { + const request = ProjectConfig.buildServerRequest(options); + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler( + this.authResourceUrlBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } + /** * Looks up a tenant by tenant ID. * @@ -2092,7 +2144,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, GET_TENANT, {}, { tenantId }) + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { return response as TenantServerResponse; }); @@ -2122,7 +2174,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (typeof request.pageToken === 'undefined') { delete request.pageToken; } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, LIST_TENANTS, request) + return this.invokeRequestHandler(this.authResourceUrlBuilder, LIST_TENANTS, request) .then((response: any) => { if (!response.tenants) { response.tenants = []; @@ -2142,7 +2194,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, undefined, { tenantId }) + return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId }) .then(() => { // Return nothing. }); @@ -2158,7 +2210,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, true); - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, CREATE_TENANT, request) + return this.invokeRequestHandler(this.authResourceUrlBuilder, CREATE_TENANT, request) .then((response: any) => { return response as TenantServerResponse; }); @@ -2184,7 +2236,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { // Do not traverse deep into testPhoneNumbers. The entire content should be replaced // and not just specific phone numbers. const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']); - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request, + return this.invokeRequestHandler(this.authResourceUrlBuilder, UPDATE_TENANT, request, { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { return response as TenantServerResponse; diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index ce45713f97..45ca3ef2d0 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1451,3 +1451,146 @@ export class OIDCConfig implements OIDCAuthProviderConfig { }; } } + +/** + * The request interface for updating a SMS Region Config. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ +export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; + +/** + * Mutual exclusive SMS Region Config of AllowByDefault interface + */ +export interface AllowByDefaultWrap { + /** + * Allow every region by default. + */ + allowByDefault: AllowByDefault; + /** @alpha */ + allowlistOnly?: never; +} + +/** + * Mutually exclusive SMS Region Config of AllowlistOnly interface + */ +export interface AllowlistOnlyWrap { + /** + * Only allowing regions by explicitly adding them to an + * allowlist. + */ + allowlistOnly: AllowlistOnly; + /** @alpha */ + allowByDefault?: never; +} + +/** + * Defines a policy of allowing every region by default and adding disallowed + * regions to a disallow list. + */ +export interface AllowByDefault { + /** + * Two letter unicode region codes to disallow as defined by + * https://cldr.unicode.org/ + * The full list of these region codes is here: + * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + disallowedRegions: string[]; +} + +/** + * Defines a policy of only allowing regions by explicitly adding them to an + * allowlist. + */ +export interface AllowlistOnly { + /** + * Two letter unicode region codes to allow as defined by + * https://cldr.unicode.org/ + * The full list of these region codes is here: + * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + allowedRegions: string[]; +} + +/** + * Defines the SMSRegionConfig class used for validation. + * + * @internal + */ +export class SmsRegionsAuthConfig { + public static validate(options: SmsRegionConfig): void { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig" must be a non-null object.', + ); + } + + const validKeys = { + allowlistOnly: true, + allowByDefault: true, + }; + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig parameter.`, + ); + } + } + + // validate mutual exclusiveness of allowByDefault and allowlistOnly + if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + 'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.', + ); + } + // validation for allowByDefault type + if (typeof options.allowByDefault !== 'undefined') { + const allowByDefaultValidKeys = { + disallowedRegions: true, + } + for (const key in options.allowByDefault) { + if (!(key in allowByDefaultValidKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`, + ); + } + } + // disallowedRegion can be empty. + if (typeof options.allowByDefault.disallowedRegions !== 'undefined' + && !validator.isArray(options.allowByDefault.disallowedRegions)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.', + ); + } + } + + if (typeof options.allowlistOnly !== 'undefined') { + const allowListOnlyValidKeys = { + allowedRegions: true, + } + for (const key in options.allowlistOnly) { + if (!(key in allowListOnlyValidKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`, + ); + } + } + + // allowedRegions can be empty + if (typeof options.allowlistOnly.allowedRegions !== 'undefined' + && !validator.isArray(options.allowlistOnly.allowedRegions)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.', + ); + } + } + } +} diff --git a/src/auth/auth.ts b/src/auth/auth.ts index d9b5aa7978..4808fbbdc0 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -19,6 +19,7 @@ import { App } from '../app/index'; import { AuthRequestHandler } from './auth-api-request'; import { TenantManager } from './tenant-manager'; import { BaseAuth } from './base-auth'; +import { ProjectConfigManager } from './project-config-manager'; /** * Auth service bound to the provided app. @@ -27,6 +28,7 @@ import { BaseAuth } from './base-auth'; export class Auth extends BaseAuth { private readonly tenantManager_: TenantManager; + private readonly projectConfigManager_: ProjectConfigManager; private readonly app_: App; /** @@ -38,6 +40,7 @@ export class Auth extends BaseAuth { super(app, new AuthRequestHandler(app)); this.app_ = app; this.tenantManager_ = new TenantManager(app); + this.projectConfigManager_ = new ProjectConfigManager(app); } /** @@ -57,4 +60,13 @@ export class Auth extends BaseAuth { public tenantManager(): TenantManager { return this.tenantManager_; } + + /** + * Returns the project config manager instance associated with the current project. + * + * @returns The project config manager instance associated with the current project. + */ + public projectConfigManager(): ProjectConfigManager { + return this.projectConfigManager_; + } } diff --git a/src/auth/index.ts b/src/auth/index.ts index 5a7e668244..7dec658473 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -61,6 +61,10 @@ export { } from './auth'; export { + AllowByDefault, + AllowByDefaultWrap, + AllowlistOnly, + AllowlistOnlyWrap, AuthFactorType, AuthProviderConfig, AuthProviderConfigFilter, @@ -81,6 +85,7 @@ export { OIDCUpdateAuthProviderRequest, SAMLAuthProviderConfig, SAMLUpdateAuthProviderRequest, + SmsRegionConfig, UserProvider, UpdateAuthProviderRequest, UpdateMultiFactorInfoRequest, @@ -116,6 +121,15 @@ export { TenantManager, } from './tenant-manager'; +export { + UpdateProjectConfigRequest, + ProjectConfig, +} from './project-config'; + +export { + ProjectConfigManager, +} from './project-config-manager'; + export { DecodedIdToken, DecodedAuthBlockingToken diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts new file mode 100644 index 0000000000..030b64a779 --- /dev/null +++ b/src/auth/project-config-manager.ts @@ -0,0 +1,67 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { App } from '../app'; +import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; +import { + AuthRequestHandler, +} from './auth-api-request'; + +/** + * Defines the project config manager used to help manage project config related operations. + * This includes: + *
    + *
  • The ability to update and get project config.
  • + */ +export class ProjectConfigManager { + private readonly authRequestHandler: AuthRequestHandler; + + /** + * Initializes a ProjectConfigManager instance for a specified FirebaseApp. + * + * @param app - The app for this ProjectConfigManager instance. + * + * @constructor + * @internal + */ + constructor(app: App) { + this.authRequestHandler = new AuthRequestHandler(app); + } + + /** + * Get the project configuration. + * + * @returns A promise fulfilled with the project configuration. + */ + public getProjectConfig(): Promise { + return this.authRequestHandler.getProjectConfig() + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } + /** + * Updates an existing project configuration. + * + * @param projectConfigOptions - The properties to update on the project. + * + * @returns A promise fulfilled with the updated project config. + */ + public updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise { + return this.authRequestHandler.updateProjectConfig(projectConfigOptions) + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } +} diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts new file mode 100644 index 0000000000..54dcfb3b9c --- /dev/null +++ b/src/auth/project-config.ts @@ -0,0 +1,131 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as validator from '../utils/validator'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { + SmsRegionsAuthConfig, + SmsRegionConfig, +} from './auth-config'; +import { deepCopy } from '../utils/deep-copy'; + +/** + * Interface representing the properties to update on the provided project config. + */ +export interface UpdateProjectConfigRequest { + /** + * The SMS configuration to update on the project. + */ + smsRegionConfig?: SmsRegionConfig; +} + +/** + * Response received from getting or updating a project config. + * This object currently exposes only the SMS Region config. + */ +export interface ProjectConfigServerResponse { + smsRegionConfig?: SmsRegionConfig; +} + +/** + * Request sent to update project config. + * This object currently exposes only the SMS Region config. + */ +export interface ProjectConfigClientRequest { + smsRegionConfig?: SmsRegionConfig; +} + +/** +* Represents a project configuration. +*/ +export class ProjectConfig { + /** + * The SMS Regions Config for the project. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ + public readonly smsRegionConfig?: SmsRegionConfig; + + /** + * Validates a project config options object. Throws an error on failure. + * + * @param request - The project config options object to validate. + */ + private static validate(request: ProjectConfigClientRequest): void { + if (!validator.isNonNullObject(request)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"UpdateProjectConfigRequest" must be a valid non-null object.', + ); + } + const validKeys = { + smsRegionConfig: true, + } + // Check for unsupported top level attributes. + for (const key in request) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${key}" is not a valid UpdateProjectConfigRequest parameter.`, + ); + } + } + // Validate SMS Regions Config if provided. + if (typeof request.smsRegionConfig !== 'undefined') { + SmsRegionsAuthConfig.validate(request.smsRegionConfig); + } + } + + /** + * Build the corresponding server request for a UpdateProjectConfigRequest object. + * @param configOptions - The properties to convert to a server request. + * @returns The equivalent server request. + * + * @internal + */ + public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest { + ProjectConfig.validate(configOptions); + return configOptions as ProjectConfigClientRequest; + } + + /** + * The Project Config object constructor. + * + * @param response - The server side response used to initialize the Project Config object. + * @constructor + * @internal + */ + constructor(response: ProjectConfigServerResponse) { + if (typeof response.smsRegionConfig !== 'undefined') { + this.smsRegionConfig = response.smsRegionConfig; + } + } + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + // JSON serialization + const json = { + smsRegionConfig: deepCopy(this.smsRegionConfig), + }; + if (typeof json.smsRegionConfig === 'undefined') { + delete json.smsRegionConfig; + } + return json; + } +} + diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index e489fa3b09..56cf2abd8d 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, - MultiFactorAuthConfig, + MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig } from './auth-config'; /** @@ -54,6 +54,11 @@ export interface UpdateTenantRequest { * Passing null clears the previously save phone number / code pairs. */ testPhoneNumbers?: { [phoneNumber: string]: string } | null; + + /** + * The SMS configuration to update on the project. + */ + smsRegionConfig?: SmsRegionConfig; } /** @@ -68,6 +73,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; + smsRegionConfig?: SmsRegionConfig; } /** The tenant server response interface. */ @@ -79,6 +85,7 @@ export interface TenantServerResponse { enableAnonymousUser?: boolean; mfaConfig?: MultiFactorAuthServerConfig; testPhoneNumbers?: {[key: string]: string}; + smsRegionConfig?: SmsRegionConfig; } /** @@ -123,6 +130,13 @@ export class Tenant { private readonly emailSignInConfig_?: EmailSignInConfig; private readonly multiFactorConfig_?: MultiFactorAuthConfig; + /** + * The SMS Regions Config to update a tenant. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ + public readonly smsRegionConfig?: SmsRegionConfig; + /** * Builds the corresponding server request for a TenantOptions object. * @@ -152,6 +166,9 @@ export class Tenant { // null will clear existing test phone numbers. Translate to empty object. request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {}; } + if (typeof tenantOptions.smsRegionConfig !== 'undefined') { + request.smsRegionConfig = tenantOptions.smsRegionConfig; + } return request; } @@ -185,6 +202,7 @@ export class Tenant { anonymousSignInEnabled: true, multiFactorConfig: true, testPhoneNumbers: true, + smsRegionConfig: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -231,6 +249,10 @@ export class Tenant { // This will throw an error if invalid. MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig); } + // Validate SMS Regions Config if provided. + if (typeof request.smsRegionConfig != 'undefined') { + SmsRegionsAuthConfig.validate(request.smsRegionConfig); + } } /** @@ -265,6 +287,9 @@ export class Tenant { if (typeof response.testPhoneNumbers !== 'undefined') { this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); } + if (typeof response.smsRegionConfig !== 'undefined') { + this.smsRegionConfig = deepCopy(response.smsRegionConfig); + } } /** @@ -294,6 +319,7 @@ export class Tenant { multiFactorConfig: this.multiFactorConfig_?.toJSON(), anonymousSignInEnabled: this.anonymousSignInEnabled, testPhoneNumbers: this.testPhoneNumbers, + smsRegionConfig: deepCopy(this.smsRegionConfig), }; if (typeof json.multiFactorConfig === 'undefined') { delete json.multiFactorConfig; @@ -301,6 +327,9 @@ export class Tenant { if (typeof json.testPhoneNumbers === 'undefined') { delete json.testPhoneNumbers; } + if (typeof json.smsRegionConfig === 'undefined') { + delete json.smsRegionConfig; + } return json; } } diff --git a/src/messaging/messaging-api-request-internal.ts b/src/messaging/messaging-api-request-internal.ts index b24154ba0c..9097ef403c 100644 --- a/src/messaging/messaging-api-request-internal.ts +++ b/src/messaging/messaging-api-request-internal.ts @@ -27,7 +27,7 @@ import { SendResponse, BatchResponse } from './messaging-api'; // FCM backend constants -const FIREBASE_MESSAGING_TIMEOUT = 10000; +const FIREBASE_MESSAGING_TIMEOUT = 15000; const FIREBASE_MESSAGING_BATCH_URL = 'https://fcm.googleapis.com/batch'; const FIREBASE_MESSAGING_HTTP_METHOD: HttpMethod = 'POST'; const FIREBASE_MESSAGING_HEADERS = { diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 78cfb62701..0184d47b4e 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import url = require('url'); +import * as url from 'url'; import * as crypto from 'crypto'; import * as bcrypt from 'bcrypt'; import * as chai from 'chai'; @@ -31,7 +31,7 @@ import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, - UserImportRecord, UserMetadata, UserRecord, getAuth, + UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, } from '../../lib/auth/index'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -57,7 +57,7 @@ const testPhoneNumber2 = '+16505550101'; const nonexistentPhoneNumber = '+18888888888'; const updatedEmail = generateRandomString(20).toLowerCase() + '@example.com'; const updatedPhone = '+16505550102'; -const customClaims: {[key: string]: any} = { +const customClaims: { [key: string]: any } = { admin: true, groupId: '1234', }; @@ -433,15 +433,15 @@ describe('admin.auth', () => { * and UserImportRecord instances. */ function mapUserRecordsToUidEmailPhones( - values: Array<{ uid: string; email?: string; phoneNumber?: string}> - ): Array<{ uid: string; email?: string; phoneNumber?: string}> { + values: Array<{ uid: string; email?: string; phoneNumber?: string }> + ): Array<{ uid: string; email?: string; phoneNumber?: string }> { return values.map((ur) => ({ uid: ur.uid, email: ur.email, phoneNumber: ur.phoneNumber })); } const testUser1 = { uid: 'uid1', email: 'user1@example.com', phoneNumber: '+15555550001' }; const testUser2 = { uid: 'uid2', email: 'user2@example.com', phoneNumber: '+15555550002' }; const testUser3 = { uid: 'uid3', email: 'user3@example.com', phoneNumber: '+15555550003' }; - const usersToCreate = [ testUser1, testUser2, testUser3 ]; + const usersToCreate = [testUser1, testUser2, testUser3]; // Also create a user with a provider config. (You can't create a user with // a provider config. But you *can* import one.) @@ -551,11 +551,11 @@ describe('admin.auth', () => { // each attempt). Occassionally, this call retrieves the user data // without the lastLoginTime/lastRefreshTime set; possibly because // it's hitting a different server than the login request uses. - let userRecord = null; + let userRecord: UserRecord | null = null; for (let i = 0; i < 3; i++) { userRecord = await getAuth().getUser('lastRefreshTimeUser'); - if (userRecord.metadata.lastRefreshTime) { + if (userRecord!['metadata']['lastRefreshTime']) { break; } @@ -564,11 +564,11 @@ describe('admin.auth', () => { }); } - const metadata = userRecord!.metadata; - expect(metadata.lastRefreshTime).to.exist; - expect(isUTCString(metadata.lastRefreshTime!)).to.be.true; - const creationTime = new Date(metadata.creationTime).getTime(); - const lastRefreshTime = new Date(metadata.lastRefreshTime!).getTime(); + const metadata = userRecord!['metadata']; + expect(metadata['lastRefreshTime']).to.exist; + expect(isUTCString(metadata['lastRefreshTime']!)).to.be.true; + const creationTime = new Date(metadata['creationTime']).getTime(); + const lastRefreshTime = new Date(metadata['lastRefreshTime']!).getTime(); expect(creationTime).lte(lastRefreshTime); expect(lastRefreshTime).lte(creationTime + 3600 * 1000); }); @@ -604,16 +604,16 @@ describe('admin.auth', () => { expect( listUsersResult.users[0].passwordHash, 'Missing passwordHash field. A common cause would be forgetting to ' - + 'add the "Firebase Authentication Admin" permission. See ' - + 'instructions in CONTRIBUTING.md', + + 'add the "Firebase Authentication Admin" permission. See ' + + 'instructions in CONTRIBUTING.md', ).to.be.ok; expect(listUsersResult.users[0].passwordHash!.length).greaterThan(0); expect( listUsersResult.users[0].passwordSalt, 'Missing passwordSalt field. A common cause would be forgetting to ' - + 'add the "Firebase Authentication Admin" permission. See ' - + 'instructions in CONTRIBUTING.md', + + 'add the "Firebase Authentication Admin" permission. See ' + + 'instructions in CONTRIBUTING.md', ).to.be.ok; expect(listUsersResult.users[0].passwordSalt!.length).greaterThan(0); @@ -704,7 +704,7 @@ describe('admin.auth', () => { // Verify ID token contents. return getAuth().verifyIdToken(idToken); }) - .then((decodedIdToken: {[key: string]: any}) => { + .then((decodedIdToken: { [key: string]: any }) => { // Confirm expected claims set on the user's ID token. for (const key in customClaims) { if (Object.prototype.hasOwnProperty.call(customClaims, key)) { @@ -728,7 +728,7 @@ describe('admin.auth', () => { // Verify ID token contents. return getAuth().verifyIdToken(idToken); }) - .then((decodedIdToken: {[key: string]: any}) => { + .then((decodedIdToken: { [key: string]: any }) => { // Confirm all custom claims are cleared. for (const key in customClaims) { if (Object.prototype.hasOwnProperty.call(customClaims, key)) { @@ -811,7 +811,7 @@ describe('admin.auth', () => { }) .then((userRecord) => { // Confirm second factors added to user. - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + const actualUserRecord: { [key: string]: any } = userRecord.toJSON(); expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2); expect(actualUserRecord.multiFactor.enrolledFactors).to.deep.equal(enrolledFactors); // Update list of second factors. @@ -823,7 +823,7 @@ describe('admin.auth', () => { }) .then((userRecord) => { expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(1); - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + const actualUserRecord: { [key: string]: any } = userRecord.toJSON(); expect(actualUserRecord.multiFactor.enrolledFactors[0]).to.deep.equal(enrolledFactors[0]); // Remove all second factors. return getAuth().updateUser(updateUser.uid, { @@ -919,7 +919,7 @@ describe('admin.auth', () => { it('A user with user record disabled is unable to sign in', async () => { const password = 'password'; const email = 'updatedEmail@example.com'; - return getAuth().updateUser(updateUser.uid, { disabled : true , password, email }) + return getAuth().updateUser(updateUser.uid, { disabled: true, password, email }) .then(() => { return clientAuth().signInWithEmailAndPassword(email, password); }) @@ -1154,7 +1154,7 @@ describe('admin.auth', () => { }); }); - it('generateVerifyAndChangeEmailLink() should return a verification link', function() { + it('generateVerifyAndChangeEmailLink() should return a verification link', function () { if (authEmulatorHost) { return this.skip(); // Not yet supported in Auth Emulator. } @@ -1180,6 +1180,61 @@ describe('admin.auth', () => { }); }); + describe('Project config management operations', () => { + before(function () { + if (authEmulatorHost) { + this.skip(); // getConfig is not supported in Auth Emulator + } + }); + const projectConfigOption1: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + } + }, + }; + const projectConfigOption2: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: ['AC', 'AD'], + } + }, + }; + const expectedProjectConfig1: any = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + } + }, + }; + const expectedProjectConfig2: any = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: ['AC', 'AD'], + } + }, + }; + + it('updateProjectConfig() should resolve with the updated project config', () => { + return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption1) + .then((actualProjectConfig) => { + expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig1); + return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption2); + }) + .then((actualProjectConfig) => { + expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig2); + }); + }); + + it('getProjectConfig() should resolve with expected project config', () => { + return getAuth().projectConfigManager().getProjectConfig() + .then((actualConfig) => { + const actualConfigObj = actualConfig.toJSON(); + expect(actualConfigObj).to.deep.equal(expectedProjectConfig2); + }); + }); + }); + describe('Tenant management operations', () => { let createdTenantId: string; const createdTenants: string[] = []; @@ -1248,12 +1303,17 @@ describe('admin.auth', () => { state: 'ENABLED', factorIds: ['phone'], }, + smsRegionConfig: { + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + } + }, }; // https://mochajs.org/ // Passing arrow functions (aka "lambdas") to Mocha is discouraged. // Lambdas lexically bind this and cannot access the Mocha context. - before(function() { + before(function () { /* tslint:disable:no-console */ if (!cmdArgs.testMultiTenancy) { // To enable, run: npm run test:integration -- --testMultiTenancy @@ -1271,7 +1331,7 @@ describe('admin.auth', () => { createdTenants.forEach((tenantId) => { promises.push( getAuth().tenantManager().deleteTenant(tenantId) - .catch(() => {/** Ignore. */})); + .catch(() => {/** Ignore. */ })); }); return Promise.all(promises); }); @@ -1285,7 +1345,7 @@ describe('admin.auth', () => { const actualTenantObj = actualTenant.toJSON(); if (authEmulatorHost) { // Not supported in Auth Emulator - delete (actualTenantObj as {testPhoneNumbers?: Record}).testPhoneNumbers; + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; delete expectedCreatedTenant.testPhoneNumbers; } expect(actualTenantObj).to.deep.equal(expectedCreatedTenant); @@ -1324,7 +1384,7 @@ describe('admin.auth', () => { const rawPassword = 'password'; const rawSalt = 'NaCl'; - before(function() { + before(function () { if (!createdTenantId) { this.skip(); } else { @@ -1432,10 +1492,10 @@ describe('admin.auth', () => { // Confirm expected user returned in the list and all users returned // belong to the expected tenant. const allUsersBelongToTenant = - listUsersResult.users.every((user) => user.tenantId === createdTenantId); + listUsersResult.users.every((user) => user.tenantId === createdTenantId); expect(allUsersBelongToTenant).to.be.true; const knownUserInTenant = - listUsersResult.users.some((user) => user.uid === createdUserUid); + listUsersResult.users.some((user) => user.uid === createdUserUid); expect(knownUserInTenant).to.be.true; }); }); @@ -1451,7 +1511,7 @@ describe('admin.auth', () => { it('importUsers() should upload a user to the specified tenant', () => { const currentHashKey = importOptions.hash.key.toString('utf8'); const passwordHash = - crypto.createHmac('sha256', currentHashKey).update(rawPassword + rawSalt).digest(); + crypto.createHmac('sha256', currentHashKey).update(rawPassword + rawSalt).digest(); const importUserRecord: any = { uid: createdUserUid, email: createdUserUid + '@example.com', @@ -1512,7 +1572,7 @@ describe('admin.auth', () => { enableRequestSigning: false, }; - before(function() { + before(function () { if (!createdTenantId) { this.skip(); } else { @@ -1593,7 +1653,7 @@ describe('admin.auth', () => { }, }; - before(function() { + before(function () { if (!createdTenantId) { this.skip(); } else { @@ -1610,7 +1670,7 @@ describe('admin.auth', () => { }); } }); - + it('should support CRUD operations', function () { // TODO(lisajian): Unskip once auth emulator supports OIDC/SAML if (authEmulatorHost) { @@ -1643,7 +1703,7 @@ describe('admin.auth', () => { const actualTenantObj = actualTenant.toJSON(); if (authEmulatorHost) { // Not supported in Auth Emulator - delete (actualTenantObj as {testPhoneNumbers?: Record}).testPhoneNumbers; + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; delete expectedCreatedTenant.testPhoneNumbers; } expect(actualTenantObj).to.deep.equal(expectedCreatedTenant); @@ -1669,13 +1729,14 @@ describe('admin.auth', () => { multiFactorConfig: deepCopy(expectedUpdatedTenant2.multiFactorConfig), // Test clearing of phone numbers. testPhoneNumbers: null, + smsRegionConfig: deepCopy(expectedUpdatedTenant2.smsRegionConfig), }; if (authEmulatorHost) { return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions) .then((actualTenant) => { const actualTenantObj = actualTenant.toJSON(); // Not supported in Auth Emulator - delete (actualTenantObj as {testPhoneNumbers?: Record}).testPhoneNumbers; + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; delete expectedUpdatedTenant.testPhoneNumbers; expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant); return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2); @@ -1683,7 +1744,7 @@ describe('admin.auth', () => { .then((actualTenant) => { const actualTenantObj = actualTenant.toJSON(); // Not supported in Auth Emulator - delete (actualTenantObj as {testPhoneNumbers?: Record}).testPhoneNumbers; + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; delete expectedUpdatedTenant2.testPhoneNumbers; expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); }); @@ -1698,6 +1759,28 @@ describe('admin.auth', () => { }); }); + it('updateTenant() should not update tenant when SMS region config is undefined', () => { + expectedUpdatedTenant.tenantId = createdTenantId; + const updatedOptions2: UpdateTenantRequest = { + displayName: expectedUpdatedTenant2.displayName, + smsRegionConfig: undefined, + }; + if (authEmulatorHost) { + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedUpdatedTenant2.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); + }); + } + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenant2); + }); + }); + it('updateTenant() should be able to enable/disable anon provider', async () => { const tenantManager = getAuth().tenantManager(); let tenant = await tenantManager.createTenant({ @@ -1799,8 +1882,8 @@ describe('admin.auth', () => { const removeTempConfigs = (): Promise => { return Promise.all([ - getAuth().deleteProviderConfig(authProviderConfig1.providerId).catch(() => {/* empty */}), - getAuth().deleteProviderConfig(authProviderConfig2.providerId).catch(() => {/* empty */}), + getAuth().deleteProviderConfig(authProviderConfig1.providerId).catch(() => {/* empty */ }), + getAuth().deleteProviderConfig(authProviderConfig2.providerId).catch(() => {/* empty */ }), ]); }; @@ -1941,8 +2024,8 @@ describe('admin.auth', () => { const removeTempConfigs = (): Promise => { return Promise.all([ - getAuth().deleteProviderConfig(authProviderConfig1.providerId).catch(() => {/* empty */}), - getAuth().deleteProviderConfig(authProviderConfig2.providerId).catch(() => {/* empty */}), + getAuth().deleteProviderConfig(authProviderConfig1.providerId).catch(() => {/* empty */ }), + getAuth().deleteProviderConfig(authProviderConfig2.providerId).catch(() => {/* empty */ }), ]); }; @@ -2249,7 +2332,7 @@ describe('admin.auth', () => { const decodedIdToken = await getAuth().verifySessionCookie(sessionCookie, true); expect(decodedIdToken.uid).to.equal(uid4); - const userRecord = await getAuth().updateUser(uid4, { disabled : true }); + const userRecord = await getAuth().updateUser(uid4, { disabled: true }); // Ensure disabled field has been updated. expect(userRecord.uid).to.equal(uid4); expect(userRecord.disabled).to.equal(true); @@ -2293,7 +2376,7 @@ describe('admin.auth', () => { let decodedIdToken = await getAuth().verifySessionCookie(sessionCookie, true); expect(decodedIdToken.uid).to.equal(uid); - const userRecord = await getAuth().updateUser(uid, { disabled : true }); + const userRecord = await getAuth().updateUser(uid, { disabled: true }); // Ensure disabled field has been updated. expect(userRecord.uid).to.equal(uid); expect(userRecord.disabled).to.equal(true); @@ -2326,9 +2409,9 @@ describe('admin.auth', () => { // Simulate a user stored using SCRYPT being migrated to Firebase Auth via importUsers. // Obtained from https://github.com/firebase/scrypt. const scryptHashKey = 'jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZ' + - 'JzxUECKbm0QsEmYUSDzZvpjeJ9WmXA=='; + 'JzxUECKbm0QsEmYUSDzZvpjeJ9WmXA=='; const scryptPasswordHash = 'V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ0' + - '8WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw=='; + '8WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw=='; const scryptHashOptions = { hash: { algorithm: 'SCRYPT', @@ -2555,7 +2638,7 @@ describe('admin.auth', () => { -readonly [k in keyof UserMetadata]: UserMetadata[k]; }; (importUserRecord.metadata as Writable).lastRefreshTime = null; - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + const actualUserRecord: { [key: string]: any } = userRecord.toJSON(); for (const key of Object.keys(importUserRecord)) { expect(JSON.stringify(actualUserRecord[key])) .to.be.equal(JSON.stringify((importUserRecord as any)[key])); @@ -2577,7 +2660,7 @@ describe('admin.auth', () => { displayName: 'Work phone number', factorId: 'phone', enrollmentTime: now, - } , + }, { uid: 'mfaUid2', phoneNumber: '+16505550002', @@ -2619,7 +2702,7 @@ describe('admin.auth', () => { return getAuth().getUser(uid); }).then((userRecord) => { // Confirm second factors added to user. - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + const actualUserRecord: { [key: string]: any } = userRecord.toJSON(); expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2); expect(actualUserRecord.multiFactor.enrolledFactors) .to.deep.equal(importUserRecord.multiFactor?.enrolledFactors); @@ -2832,7 +2915,7 @@ async function deleteUsersWithDelay(uids: string[]): Promise * @param expected object. * @param actual object. */ -function assertDeepEqualUnordered(expected: {[key: string]: any}, actual: {[key: string]: any}): void { +function assertDeepEqualUnordered(expected: { [key: string]: any }, actual: { [key: string]: any }): void { for (const key in expected) { if (Object.prototype.hasOwnProperty.call(expected, key)) { expect(actual[key]) diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts new file mode 100644 index 0000000000..d06b24fa80 --- /dev/null +++ b/test/unit/auth/project-config-manager.spec.ts @@ -0,0 +1,196 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { ProjectConfigManager } from '../../../src/auth/project-config-manager'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfigManager', () => { + let mockApp: FirebaseApp; + let projectConfigManager: ProjectConfigManager; + let nullAccessTokenProjectConfigManager: ProjectConfigManager; + let malformedAccessTokenProjectConfigManager: ProjectConfigManager; + let rejectedPromiseAccessTokenProjectConfigManager: ProjectConfigManager; + const GET_CONFIG_RESPONSE: ProjectConfigServerResponse = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + before(() => { + mockApp = mocks.app(); + projectConfigManager = new ProjectConfigManager(mockApp); + nullAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningNullAccessToken()); + malformedAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appRejectedWhileFetchingAccessToken()); + }); + + after(() => { + return mockApp.delete(); + }); + + describe('getProjectConfig()', () => { + const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Project Config on success', () => { + // Stub getProjectConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected project config returned. + expect(result).to.deep.equal(expectedProjectConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('updateProjectConfig()', () => { + const projectConfigOptions: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to update the config provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no projectConfigOptions', () => { + return (projectConfigManager as any).updateProjectConfig(null as unknown as UpdateProjectConfigRequest) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a ProjectConfig on updateProjectConfig request success', () => { + // Stub updateProjectConfig to return expected result. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then((actualProjectConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected Project Config object returned. + expect(actualProjectConfig).to.deep.equal(expectedProjectConfig); + }); + }); + + it('should throw an error when updateProjectConfig returns an error', () => { + // Stub updateProjectConfig to throw a backend error. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') + .returns(Promise.reject(expectedError)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts new file mode 100644 index 0000000000..19cc8f420d --- /dev/null +++ b/test/unit/auth/project-config.spec.ts @@ -0,0 +1,192 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { deepCopy } from '../../../src/utils/deep-copy'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest, +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfig', () => { + const serverResponse: ProjectConfigServerResponse = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + const updateProjectConfigRequest1: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + const updateProjectConfigRequest2: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + const updateProjectConfigRequest3: any = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + }, + }, + }; + + describe('buildServerRequest()', () => { + + describe('for an update request', () => { + it('should throw on null SmsRegionConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; + configOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; + configOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest3) as any; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + + it('should not throw on valid client request object', () => { + const configOptionsClientRequest1 = deepCopy(updateProjectConfigRequest1); + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest1); + }).not.to.throw; + const configOptionsClientRequest2 = deepCopy(updateProjectConfigRequest2); + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest2); + }).not.to.throw; + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { + expect(() => { + ProjectConfig.buildServerRequest(request as any); + }).to.throw('"UpdateProjectConfigRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for update request', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.unsupported = 'value'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"unsupported" is not a valid UpdateProjectConfigRequest parameter.'); + }); + }); + }); + + describe('constructor', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + const projectConfig = new ProjectConfig(serverResponseCopy); + + it('should not throw on valid initialization', () => { + expect(() => new ProjectConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly property smsRegionConfig', () => { + const expectedSmsRegionConfig = { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }; + expect(projectConfig.smsRegionConfig).to.deep.equal(expectedSmsRegionConfig); + }); + }); + + describe('toJSON()', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + it('should return the expected object representation of project config', () => { + expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ + smsRegionConfig: deepCopy(serverResponse.smsRegionConfig) + }); + }); + + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.smsRegionConfig; + + expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({}); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts index 3843a41bb7..44885ecafa 100644 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -33,6 +33,18 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('Tenant', () => { + const smsAllowByDefault = { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }; + + const smsAllowlistOnly = { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }; + const serverRequest: TenantServerResponse = { name: 'projects/project1/tenants/TENANT-ID', displayName: 'TENANT-DISPLAY-NAME', @@ -46,6 +58,7 @@ describe('Tenant', () => { '+16505551234': '019287', '+16505550676': '985235', }, + smsRegionConfig: smsAllowByDefault, }; const clientRequest: UpdateTenantRequest = { @@ -62,6 +75,7 @@ describe('Tenant', () => { '+16505551234': '019287', '+16505550676': '985235', }, + smsRegionConfig: smsAllowByDefault, }; const serverRequestWithoutMfa: TenantServerResponse = { @@ -141,6 +155,64 @@ describe('Tenant', () => { .to.deep.equal(tenantOptionsServerRequest); }); + it('should throw on null SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); expect(() => { @@ -232,6 +304,64 @@ describe('Tenant', () => { }).to.throw('"CreateTenantRequest.testPhoneNumbers" must be a non-null object.'); }); + it('should throw on null SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { @@ -314,6 +444,11 @@ describe('Tenant', () => { deepCopy(clientRequest.testPhoneNumbers)); }); + it('should set readonly property smsRegionConfig', () => { + expect(tenant.smsRegionConfig).to.deep.equal( + deepCopy(clientRequest.smsRegionConfig)); + }); + it('should throw when no tenant ID is provided', () => { const invalidOptions = deepCopy(serverRequest); // Use resource name that does not include a tenant ID. @@ -352,6 +487,7 @@ describe('Tenant', () => { anonymousSignInEnabled: false, multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), + smsRegionConfig: deepCopy(clientRequest.smsRegionConfig), }); }); @@ -359,6 +495,7 @@ describe('Tenant', () => { const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequest); delete serverRequestCopyWithoutMfa.mfaConfig; delete serverRequestCopyWithoutMfa.testPhoneNumbers; + delete serverRequestCopyWithoutMfa.smsRegionConfig; expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ tenantId: 'TENANT-ID', diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 03f413825a..df459c6ec3 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -3538,7 +3538,7 @@ describe('Messaging', () => { expect(httpsRequestStub).to.have.been.calledOnce.and.calledWith({ method: 'POST', data: { message: expectedReq }, - timeout: 10000, + timeout: 15000, url: 'https://fcm.googleapis.com/v1/projects/project_id/messages:send', headers: expectedHeaders, });