Skip to content

Commit f94423f

Browse files
bojeil-googlehiranya911
authored andcommitted
Starts defining multi-tenancy APIs. This includes: (#526)
* Starts defining multi-tenancy APIs. This includes: - Defining type definitions. - Adding tenantId to UserRecord and UserImportBuilder. - Adding new errors associated with tenant operations. - Defines the Tenant object. As the changes are quite large. This will be split into multiple PRs. * Minor fixes and tweaks. * Addresses comments from review. * Addresses review comments.
1 parent 2e48327 commit f94423f

File tree

11 files changed

+811
-17
lines changed

11 files changed

+811
-17
lines changed

src/auth/auth-config.ts

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,115 @@ export interface OIDCAuthProviderRequest extends OIDCUpdateAuthProviderRequest {
149149
/** The public API request interface for updating a generic Auth provider. */
150150
export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest;
151151

152+
/** The email provider configuration interface. */
153+
export interface EmailSignInProviderConfig {
154+
enabled?: boolean;
155+
passwordRequired?: boolean; // In the backend API, default is true if not provided
156+
}
157+
158+
/** The server side email configuration request interface. */
159+
export interface EmailSignInConfigServerRequest {
160+
allowPasswordSignup?: boolean;
161+
enableEmailLinkSignin?: boolean;
162+
}
163+
164+
165+
/**
166+
* Defines the email sign-in config class used to convert client side EmailSignInConfig
167+
* to a format that is understood by the Auth server.
168+
*/
169+
export class EmailSignInConfig implements EmailSignInProviderConfig {
170+
public readonly enabled?: boolean;
171+
public readonly passwordRequired?: boolean;
172+
173+
/**
174+
* Static method to convert a client side request to a EmailSignInConfigServerRequest.
175+
* Throws an error if validation fails.
176+
*
177+
* @param {any} options The options object to convert to a server request.
178+
* @return {EmailSignInConfigServerRequest} The resulting server request.
179+
*/
180+
public static buildServerRequest(options: EmailSignInProviderConfig): EmailSignInConfigServerRequest {
181+
const request: EmailSignInConfigServerRequest = {};
182+
EmailSignInConfig.validate(options);
183+
if (options.hasOwnProperty('enabled')) {
184+
request.allowPasswordSignup = options.enabled;
185+
}
186+
if (options.hasOwnProperty('passwordRequired')) {
187+
request.enableEmailLinkSignin = !options.passwordRequired;
188+
}
189+
return request;
190+
}
191+
192+
/**
193+
* Validates the EmailSignInConfig options object. Throws an error on failure.
194+
*
195+
* @param {any} options The options object to validate.
196+
*/
197+
private static validate(options: {[key: string]: any}) {
198+
// TODO: Validate the request.
199+
const validKeys = {
200+
enabled: true,
201+
passwordRequired: true,
202+
};
203+
if (!validator.isNonNullObject(options)) {
204+
throw new FirebaseAuthError(
205+
AuthClientErrorCode.INVALID_ARGUMENT,
206+
'"EmailSignInConfig" must be a non-null object.',
207+
);
208+
}
209+
// Check for unsupported top level attributes.
210+
for (const key in options) {
211+
if (!(key in validKeys)) {
212+
throw new FirebaseAuthError(
213+
AuthClientErrorCode.INVALID_ARGUMENT,
214+
`"${key}" is not a valid EmailSignInConfig parameter.`,
215+
);
216+
}
217+
}
218+
// Validate content.
219+
if (typeof options.enabled !== 'undefined' &&
220+
!validator.isBoolean(options.enabled)) {
221+
throw new FirebaseAuthError(
222+
AuthClientErrorCode.INVALID_ARGUMENT,
223+
'"EmailSignInConfig.enabled" must be a boolean.',
224+
);
225+
}
226+
if (typeof options.passwordRequired !== 'undefined' &&
227+
!validator.isBoolean(options.passwordRequired)) {
228+
throw new FirebaseAuthError(
229+
AuthClientErrorCode.INVALID_ARGUMENT,
230+
'"EmailSignInConfig.passwordRequired" must be a boolean.',
231+
);
232+
}
233+
}
234+
235+
/**
236+
* The EmailSignInConfig constructor.
237+
*
238+
* @param {any} response The server side response used to initialize the
239+
* EmailSignInConfig object.
240+
* @constructor
241+
*/
242+
constructor(response: {[key: string]: any}) {
243+
if (typeof response.allowPasswordSignup === 'undefined') {
244+
throw new FirebaseAuthError(
245+
AuthClientErrorCode.INTERNAL_ERROR,
246+
'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response');
247+
}
248+
this.enabled = response.allowPasswordSignup;
249+
this.passwordRequired = !response.enableEmailLinkSignin;
250+
}
251+
252+
/** @return {object} The plain object representation of the email sign-in config. */
253+
public toJSON(): object {
254+
return {
255+
enabled: this.enabled,
256+
passwordRequired: this.passwordRequired,
257+
};
258+
}
259+
}
260+
152261

153262
/**
154263
* Defines the SAMLConfig class used to convert a client side configuration to its
@@ -367,24 +476,24 @@ export class SAMLConfig implements SAMLAuthProviderConfig {
367476
AuthClientErrorCode.INTERNAL_ERROR,
368477
'INTERNAL ASSERT FAILED: Invalid SAML configuration response');
369478
}
370-
utils.addReadonlyGetter(this, 'providerId', SAMLConfig.getProviderIdFromResourceName(response.name));
479+
this.providerId = SAMLConfig.getProviderIdFromResourceName(response.name);
371480
// RP config.
372-
utils.addReadonlyGetter(this, 'rpEntityId', response.spConfig.spEntityId);
373-
utils.addReadonlyGetter(this, 'callbackURL', response.spConfig.callbackUri);
481+
this.rpEntityId = response.spConfig.spEntityId;
482+
this.callbackURL = response.spConfig.callbackUri;
374483
// IdP config.
375-
utils.addReadonlyGetter(this, 'idpEntityId', response.idpConfig.idpEntityId);
376-
utils.addReadonlyGetter(this, 'ssoURL', response.idpConfig.ssoUrl);
377-
utils.addReadonlyGetter(this, 'enableRequestSigning', !!response.idpConfig.signRequest);
484+
this.idpEntityId = response.idpConfig.idpEntityId;
485+
this.ssoURL = response.idpConfig.ssoUrl;
486+
this.enableRequestSigning = !!response.idpConfig.signRequest;
378487
const x509Certificates: string[] = [];
379488
for (const cert of (response.idpConfig.idpCertificates || [])) {
380489
if (cert.x509Certificate) {
381490
x509Certificates.push(cert.x509Certificate);
382491
}
383492
}
384-
utils.addReadonlyGetter(this, 'x509Certificates', x509Certificates);
493+
this.x509Certificates = x509Certificates;
385494
// When enabled is undefined, it takes its default value of false.
386-
utils.addReadonlyGetter(this, 'enabled', !!response.enabled);
387-
utils.addReadonlyGetter(this, 'displayName', response.displayName);
495+
this.enabled = !!response.enabled;
496+
this.displayName = response.displayName;
388497
}
389498

390499
/** @return {SAMLAuthProviderConfig} The plain object representation of the SAMLConfig. */
@@ -555,12 +664,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
555664
AuthClientErrorCode.INTERNAL_ERROR,
556665
'INTERNAL ASSERT FAILED: Invalid OIDC configuration response');
557666
}
558-
utils.addReadonlyGetter(this, 'providerId', OIDCConfig.getProviderIdFromResourceName(response.name));
559-
utils.addReadonlyGetter(this, 'clientId', response.clientId);
560-
utils.addReadonlyGetter(this, 'issuer', response.issuer);
667+
this.providerId = OIDCConfig.getProviderIdFromResourceName(response.name);
668+
this.clientId = response.clientId;
669+
this.issuer = response.issuer;
561670
// When enabled is undefined, it takes its default value of false.
562-
utils.addReadonlyGetter(this, 'enabled', !!response.enabled);
563-
utils.addReadonlyGetter(this, 'displayName', response.displayName);
671+
this.enabled = !!response.enabled;
672+
this.displayName = response.displayName;
564673
}
565674

566675
/** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */

src/auth/tenant.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/*!
2+
* Copyright 2019 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as utils from '../utils';
18+
import * as validator from '../utils/validator';
19+
import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error';
20+
import {
21+
EmailSignInConfig, EmailSignInConfigServerRequest, EmailSignInProviderConfig,
22+
} from './auth-config';
23+
24+
/** The server side tenant type enum. */
25+
export type TenantServerType = 'LIGHTWEIGHT' | 'FULL_SERVICE' | 'TYPE_UNSPECIFIED';
26+
27+
/** The client side tenant type enum. */
28+
export type TenantType = 'lightweight' | 'full_service' | 'type_unspecified';
29+
30+
/** The TenantOptions interface used for create/read/update tenant operations. */
31+
export interface TenantOptions {
32+
displayName?: string;
33+
type?: TenantType;
34+
emailSignInConfig?: EmailSignInProviderConfig;
35+
}
36+
37+
/** The corresponding server side representation of a TenantOptions object. */
38+
export interface TenantOptionsServerRequest extends EmailSignInConfigServerRequest {
39+
displayName?: string;
40+
type?: TenantServerType;
41+
}
42+
43+
/** The tenant server response interface. */
44+
export interface TenantServerResponse {
45+
name: string;
46+
type?: TenantServerType;
47+
displayName?: string;
48+
allowPasswordSignup: boolean;
49+
enableEmailLinkSignin: boolean;
50+
}
51+
52+
/** The interface representing the listTenant API response. */
53+
export interface ListTenantsResult {
54+
tenants: Tenant[];
55+
pageToken?: string;
56+
}
57+
58+
59+
/**
60+
* Tenant class that defines a Firebase Auth tenant.
61+
*/
62+
export class Tenant {
63+
public readonly tenantId: string;
64+
public readonly type?: TenantType;
65+
public readonly displayName?: string;
66+
public readonly emailSignInConfig?: EmailSignInConfig;
67+
68+
/**
69+
* Builds the corresponding server request for a TenantOptions object.
70+
*
71+
* @param {TenantOptions} tenantOptions The properties to convert to a server request.
72+
* @param {boolean} createRequest Whether this is a create request.
73+
* @return {object} The equivalent server request.
74+
*/
75+
public static buildServerRequest(
76+
tenantOptions: TenantOptions, createRequest: boolean): TenantOptionsServerRequest {
77+
Tenant.validate(tenantOptions, createRequest);
78+
let request: TenantOptionsServerRequest = {};
79+
if (typeof tenantOptions.emailSignInConfig !== 'undefined') {
80+
request = EmailSignInConfig.buildServerRequest(tenantOptions.emailSignInConfig);
81+
}
82+
if (typeof tenantOptions.displayName !== 'undefined') {
83+
request.displayName = tenantOptions.displayName;
84+
}
85+
if (typeof tenantOptions.type !== 'undefined') {
86+
request.type = tenantOptions.type.toUpperCase() as TenantServerType;
87+
}
88+
return request;
89+
}
90+
91+
/**
92+
* Returns the tenant ID corresponding to the resource name if available.
93+
*
94+
* @param {string} resourceName The server side resource name
95+
* @return {?string} The tenant ID corresponding to the resource, null otherwise.
96+
*/
97+
public static getTenantIdFromResourceName(resourceName: string): string | null {
98+
// name is of form projects/project1/tenants/tenant1
99+
const matchTenantRes = resourceName.match(/\/tenants\/(.*)$/);
100+
if (!matchTenantRes || matchTenantRes.length < 2) {
101+
return null;
102+
}
103+
return matchTenantRes[1];
104+
}
105+
106+
/**
107+
* Validates a tenant options object. Throws an error on failure.
108+
*
109+
* @param {any} request The tenant options object to validate.
110+
* @param {boolean} createRequest Whether this is a create request.
111+
*/
112+
private static validate(request: any, createRequest: boolean) {
113+
const validKeys = {
114+
displayName: true,
115+
type: true,
116+
emailSignInConfig: true,
117+
};
118+
const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest';
119+
if (!validator.isNonNullObject(request)) {
120+
throw new FirebaseAuthError(
121+
AuthClientErrorCode.INVALID_ARGUMENT,
122+
`"${label}" must be a valid non-null object.`,
123+
);
124+
}
125+
// Check for unsupported top level attributes.
126+
for (const key in request) {
127+
if (!(key in validKeys)) {
128+
throw new FirebaseAuthError(
129+
AuthClientErrorCode.INVALID_ARGUMENT,
130+
`"${key}" is not a valid ${label} parameter.`,
131+
);
132+
}
133+
}
134+
// Validate displayName type if provided.
135+
if (typeof request.displayName !== 'undefined' &&
136+
!validator.isNonEmptyString(request.displayName)) {
137+
throw new FirebaseAuthError(
138+
AuthClientErrorCode.INVALID_ARGUMENT,
139+
`"${label}.displayName" must be a valid non-empty string.`,
140+
);
141+
}
142+
// Validate type if provided.
143+
if (typeof request.type !== 'undefined' && !createRequest) {
144+
throw new FirebaseAuthError(
145+
AuthClientErrorCode.INVALID_ARGUMENT,
146+
'"Tenant.type" is an immutable property.',
147+
);
148+
}
149+
if (createRequest &&
150+
request.type !== 'full_service' &&
151+
request.type !== 'lightweight') {
152+
throw new FirebaseAuthError(
153+
AuthClientErrorCode.INVALID_ARGUMENT,
154+
`"${label}.type" must be either "full_service" or "lightweight".`,
155+
);
156+
}
157+
// Validate emailSignInConfig type if provided.
158+
if (typeof request.emailSignInConfig !== 'undefined') {
159+
// This will throw an error if invalid.
160+
EmailSignInConfig.buildServerRequest(request.emailSignInConfig);
161+
}
162+
}
163+
164+
/**
165+
* The Tenant object constructor.
166+
*
167+
* @param {any} response The server side response used to initialize the Tenant object.
168+
* @constructor
169+
*/
170+
constructor(response: any) {
171+
const tenantId = Tenant.getTenantIdFromResourceName(response.name);
172+
if (!tenantId) {
173+
throw new FirebaseAuthError(
174+
AuthClientErrorCode.INTERNAL_ERROR,
175+
'INTERNAL ASSERT FAILED: Invalid tenant response',
176+
);
177+
}
178+
this.tenantId = tenantId;
179+
this.displayName = response.displayName;
180+
this.type = (response.type && response.type.toLowerCase()) || undefined;
181+
try {
182+
this.emailSignInConfig = new EmailSignInConfig(response);
183+
} catch (e) {
184+
this.emailSignInConfig = undefined;
185+
}
186+
}
187+
188+
/** @return {object} The plain object representation of the tenant. */
189+
public toJSON(): object {
190+
return {
191+
tenantId: this.tenantId,
192+
displayName: this.displayName,
193+
type: this.type,
194+
emailSignInConfig: this.emailSignInConfig && this.emailSignInConfig.toJSON(),
195+
};
196+
}
197+
}
198+

src/auth/user-import-builder.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface UserImportRecord {
6363
customClaims?: object;
6464
passwordHash?: Buffer;
6565
passwordSalt?: Buffer;
66+
tenantId?: string;
6667
}
6768

6869

@@ -87,6 +88,7 @@ interface UploadAccountUser {
8788
lastLoginAt?: number;
8889
createdAt?: number;
8990
customAttributes?: string;
91+
tenantId?: string;
9092
}
9193

9294

@@ -153,6 +155,7 @@ function populateUploadAccountUser(
153155
photoUrl: user.photoURL,
154156
phoneNumber: user.phoneNumber,
155157
providerUserInfo: [],
158+
tenantId: user.tenantId,
156159
customAttributes: user.customClaims && JSON.stringify(user.customClaims),
157160
};
158161
if (typeof user.passwordHash !== 'undefined') {

0 commit comments

Comments
 (0)