diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 97fffbd50f..be4a99b44b 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -736,43 +736,30 @@ export namespace remoteConfig { export const SDK_VERSION: string; // @public -export function securityRules(app?: app.App): securityRules.SecurityRules; +export function securityRules(app?: App): securityRules.SecurityRules; // @public (undocumented) export namespace securityRules { - export interface Ruleset extends RulesetMetadata { - // (undocumented) - readonly source: RulesFile[]; - } - export interface RulesetMetadata { - readonly createTime: string; - readonly name: string; - } - export interface RulesetMetadataList { - readonly nextPageToken?: string; - readonly rulesets: RulesetMetadata[]; - } - export interface RulesFile { - // (undocumented) - readonly content: string; - // (undocumented) - readonly name: string; - } - export interface SecurityRules { - // (undocumented) - app: app.App; - createRuleset(file: RulesFile): Promise; - createRulesFileFromSource(name: string, source: string | Buffer): RulesFile; - deleteRuleset(name: string): Promise; - getFirestoreRuleset(): Promise; - getRuleset(name: string): Promise; - getStorageRuleset(bucket?: string): Promise; - listRulesetMetadata(pageSize?: number, nextPageToken?: string): Promise; - releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise; - releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; - releaseStorageRuleset(ruleset: string | RulesetMetadata, bucket?: string): Promise; - releaseStorageRulesetFromSource(source: string | Buffer, bucket?: string): Promise; - } + // Warning: (ae-forgotten-export) The symbol "Ruleset" needs to be exported by the entry point default-namespace.d.ts + // + // (undocumented) + export type Ruleset = Ruleset; + // Warning: (ae-forgotten-export) The symbol "RulesetMetadata" needs to be exported by the entry point default-namespace.d.ts + // + // (undocumented) + export type RulesetMetadata = RulesetMetadata; + // Warning: (ae-forgotten-export) The symbol "RulesetMetadataList" needs to be exported by the entry point default-namespace.d.ts + // + // (undocumented) + export type RulesetMetadataList = RulesetMetadataList; + // Warning: (ae-forgotten-export) The symbol "RulesFile" needs to be exported by the entry point default-namespace.d.ts + // + // (undocumented) + export type RulesFile = RulesFile; + // Warning: (ae-forgotten-export) The symbol "SecurityRules" needs to be exported by the entry point default-namespace.d.ts + // + // (undocumented) + export type SecurityRules = SecurityRules; } // @public (undocumented) diff --git a/etc/firebase-admin.security-rules.api.md b/etc/firebase-admin.security-rules.api.md new file mode 100644 index 0000000000..81310c26c3 --- /dev/null +++ b/etc/firebase-admin.security-rules.api.md @@ -0,0 +1,83 @@ +## API Report File for "firebase-admin.security-rules" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Agent } from 'http'; + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function getSecurityRules(app?: App): SecurityRules; + +// @public +export class Ruleset { + // (undocumented) + readonly createTime: string; + // (undocumented) + readonly name: string; + // (undocumented) + readonly source: RulesFile[]; +} + +// @public +export interface RulesetMetadata { + readonly createTime: string; + readonly name: string; +} + +// @public (undocumented) +export class RulesetMetadataList { + // (undocumented) + readonly nextPageToken?: string; + // (undocumented) + readonly rulesets: RulesetMetadata[]; +} + +// @public +export interface RulesFile { + // (undocumented) + readonly content: string; + // (undocumented) + readonly name: string; +} + +// @public +export class SecurityRules { + // (undocumented) + readonly app: App; + createRuleset(file: RulesFile): Promise; + createRulesFileFromSource(name: string, source: string | Buffer): RulesFile; + deleteRuleset(name: string): Promise; + getFirestoreRuleset(): Promise; + getRuleset(name: string): Promise; + getStorageRuleset(bucket?: string): Promise; + listRulesetMetadata(pageSize?: number, nextPageToken?: string): Promise; + releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise; + releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; + releaseStorageRuleset(ruleset: string | RulesetMetadata, bucket?: string): Promise; + releaseStorageRulesetFromSource(source: string | Buffer, bucket?: string): Promise; +} + +// @public +export function securityRules(app?: App): securityRules.SecurityRules; + +// @public (undocumented) +export namespace securityRules { + // (undocumented) + export type Ruleset = Ruleset; + // (undocumented) + export type RulesetMetadata = RulesetMetadata; + // (undocumented) + export type RulesetMetadataList = RulesetMetadataList; + // (undocumented) + export type RulesFile = RulesFile; + // (undocumented) + export type SecurityRules = SecurityRules; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/generate-reports.js b/generate-reports.js index 86255822d7..3184a3845f 100644 --- a/generate-reports.js +++ b/generate-reports.js @@ -40,6 +40,7 @@ const entryPoints = { 'firebase-admin/firestore': './lib/firestore/index.d.ts', 'firebase-admin/instance-id': './lib/instance-id/index.d.ts', 'firebase-admin/messaging': './lib/messaging/index.d.ts', + 'firebase-admin/security-rules': './lib/security-rules/index.d.ts', 'firebase-admin/remote-config': './lib/remote-config/index.d.ts', }; diff --git a/gulpfile.js b/gulpfile.js index a2268bc070..1f1bff4f6c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -93,6 +93,7 @@ gulp.task('compile', function() { 'lib/firestore/*.d.ts', 'lib/instance-id/*.d.ts', 'lib/messaging/*.d.ts', + 'lib/security-rules/*.d.ts', 'lib/remote-config/*.d.ts', '!lib/utils/index.d.ts', ]; diff --git a/src/app/firebase-app.ts b/src/app/firebase-app.ts index 0dbe4c04c6..005061de09 100644 --- a/src/app/firebase-app.ts +++ b/src/app/firebase-app.ts @@ -31,7 +31,7 @@ import { Database } from '../database/index'; import { Firestore } from '../firestore/index'; import { InstanceId } from '../instance-id/index'; import { ProjectManagement } from '../project-management/project-management'; -import { SecurityRules } from '../security-rules/security-rules'; +import { SecurityRules } from '../security-rules/index'; import { RemoteConfig } from '../remote-config/index'; /** @@ -356,11 +356,8 @@ export class FirebaseApp implements app.App { * @return The SecurityRules service instance of this app. */ public securityRules(): SecurityRules { - return this.ensureService_('security-rules', () => { - const securityRulesService: typeof SecurityRules = - require('../security-rules/security-rules').SecurityRules; - return new securityRulesService(this); - }); + const fn = require('../security-rules/index').getSecurityRules; + return fn(this); } /** diff --git a/src/security-rules/index.ts b/src/security-rules/index.ts index 2871f7873b..0af1ef8feb 100644 --- a/src/security-rules/index.ts +++ b/src/security-rules/index.ts @@ -14,7 +14,34 @@ * limitations under the License. */ -import { app } from '../firebase-namespace-api'; +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { SecurityRules } from './security-rules'; + +export { + RulesFile, + Ruleset, + RulesetMetadata, + RulesetMetadataList, + SecurityRules, +} from './security-rules'; + +import { + RulesFile as TRulesFile, + Ruleset as TRuleset, + RulesetMetadata as TRulesetMetadata, + RulesetMetadataList as TRulesetMetadataList, + SecurityRules as TSecurityRules, +} from './security-rules'; + +export function getSecurityRules(app?: App): SecurityRules { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('securityRules', (app) => new SecurityRules(app)); +} /** * Gets the {@link securityRules.SecurityRules @@ -44,193 +71,13 @@ import { app } from '../firebase-namespace-api'; * @return The default `SecurityRules` service if no app is provided, or the * `SecurityRules` service associated with the provided app. */ -export declare function securityRules(app?: app.App): securityRules.SecurityRules; +export declare function securityRules(app?: App): securityRules.SecurityRules; /* eslint-disable @typescript-eslint/no-namespace */ export namespace securityRules { - /** - * A source file containing some Firebase security rules. The content includes raw - * source code including text formatting, indentation and comments. Use the - * [`securityRules.createRulesFileFromSource()`](securityRules.SecurityRules#createRulesFileFromSource) - * method to create new instances of this type. - */ - export interface RulesFile { - readonly name: string; - readonly content: string; - } - - /** - * Required metadata associated with a ruleset. - */ - export interface RulesetMetadata { - /** - * Name of the `Ruleset` as a short string. This can be directly passed into APIs - * like {@link securityRules.SecurityRules.getRuleset `securityRules.getRuleset()`} - * and {@link securityRules.SecurityRules.deleteRuleset `securityRules.deleteRuleset()`}. - */ - readonly name: string; - /** - * Creation time of the `Ruleset` as a UTC timestamp string. - */ - readonly createTime: string; - } - - /** - * A page of ruleset metadata. - */ - export interface RulesetMetadataList { - /** - * A batch of ruleset metadata. - */ - readonly rulesets: RulesetMetadata[]; - /** - * The next page token if available. This is needed to retrieve the next batch. - */ - readonly nextPageToken?: string; - } - - /** - * A set of Firebase security rules. - */ - export interface Ruleset extends RulesetMetadata { - readonly source: RulesFile[]; - } - - /** - * The Firebase `SecurityRules` service interface. - */ - export interface SecurityRules { - app: app.App; - - /** - * Creates a {@link securityRules.RulesFile `RuleFile`} with the given name - * and source. Throws an error if any of the arguments are invalid. This is a local - * operation, and does not involve any network API calls. - * - * @example - * ```javascript - * const source = '// Some rules source'; - * const rulesFile = admin.securityRules().createRulesFileFromSource( - * 'firestore.rules', source); - * ``` - * - * @param name Name to assign to the rules file. This is usually a short file name that - * helps identify the file in a ruleset. - * @param source Contents of the rules file. - * @return A new rules file instance. - */ - createRulesFileFromSource(name: string, source: string | Buffer): RulesFile; - - /** - * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given - * {@link securityRules.RulesFile `RuleFile`}. - * - * @param file Rules file to include in the new `Ruleset`. - * @returns A promise that fulfills with the newly created `Ruleset`. - */ - createRuleset(file: RulesFile): Promise; - - /** - * Gets the {@link securityRules.Ruleset `Ruleset`} identified by the given - * name. The input name should be the short name string without the project ID - * prefix. For example, to retrieve the `projects/project-id/rulesets/my-ruleset`, - * pass the short name "my-ruleset". Rejects with a `not-found` error if the - * specified `Ruleset` cannot be found. - * - * @param name Name of the `Ruleset` to retrieve. - * @return A promise that fulfills with the specified `Ruleset`. - */ - getRuleset(name: string): Promise; - - /** - * Deletes the {@link securityRules.Ruleset `Ruleset`} identified by the given - * name. The input name should be the short name string without the project ID - * prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, - * pass the short name "my-ruleset". Rejects with a `not-found` error if the - * specified `Ruleset` cannot be found. - * - * @param name Name of the `Ruleset` to delete. - * @return A promise that fulfills when the `Ruleset` is deleted. - */ - deleteRuleset(name: string): Promise; - - /** - * Retrieves a page of ruleset metadata. - * - * @param pageSize The page size, 100 if undefined. This is also the maximum allowed - * limit. - * @param nextPageToken The next page token. If not specified, returns rulesets - * starting without any offset. - * @return A promise that fulfills with a page of rulesets. - */ - listRulesetMetadata( - pageSize?: number, nextPageToken?: string): Promise; - - /** - * Gets the {@link securityRules.Ruleset `Ruleset`} currently applied to - * Cloud Firestore. Rejects with a `not-found` error if no ruleset is applied - * on Firestore. - * - * @return A promise that fulfills with the Firestore ruleset. - */ - getFirestoreRuleset(): Promise; - - /** - * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given - * source, and applies it to Cloud Firestore. - * - * @param source Rules source to apply. - * @return A promise that fulfills when the ruleset is created and released. - */ - releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; - - /** - * Applies the specified {@link securityRules.Ruleset `Ruleset`} ruleset - * to Cloud Firestore. - * - * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object - * containing the name. - * @return A promise that fulfills when the ruleset is released. - */ - releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise; - - /** - * Gets the {@link securityRules.Ruleset `Ruleset`} currently applied to a - * Cloud Storage bucket. Rejects with a `not-found` error if no ruleset is applied - * on the bucket. - * - * @param bucket Optional name of the Cloud Storage bucket to be retrieved. If not - * specified, retrieves the ruleset applied on the default bucket configured via - * `AppOptions`. - * @return A promise that fulfills with the Cloud Storage ruleset. - */ - getStorageRuleset(bucket?: string): Promise; - - /** - * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given - * source, and applies it to a Cloud Storage bucket. - * - * @param source Rules source to apply. - * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If - * not specified, applies the ruleset on the default bucket configured via - * {@link AppOptions `AppOptions`}. - * @return A promise that fulfills when the ruleset is created and released. - */ - releaseStorageRulesetFromSource( - source: string | Buffer, bucket?: string): Promise; - - /** - * Applies the specified {@link securityRules.Ruleset `Ruleset`} ruleset - * to a Cloud Storage bucket. - * - * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object - * containing the name. - * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If - * not specified, applies the ruleset on the default bucket configured via - * {@link AppOptions `AppOptions`}. - * @return A promise that fulfills when the ruleset is released. - */ - releaseStorageRuleset( - ruleset: string | RulesetMetadata, bucket?: string): Promise; - } + export type RulesFile = TRulesFile; + export type Ruleset = TRuleset; + export type RulesetMetadata = TRulesetMetadata; + export type RulesetMetadataList = TRulesetMetadataList; + export type SecurityRules = TSecurityRules; } diff --git a/src/security-rules/security-rules-api-client-internal.ts b/src/security-rules/security-rules-api-client-internal.ts index 1719e64316..73369f0918 100644 --- a/src/security-rules/security-rules-api-client-internal.ts +++ b/src/security-rules/security-rules-api-client-internal.ts @@ -20,6 +20,7 @@ import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-r import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseApp } from '../app/firebase-app'; +import { App } from '../app'; const RULES_V1_API = 'https://firebaserules.googleapis.com/v1'; const FIREBASE_VERSION_HEADER = { @@ -59,7 +60,7 @@ export class SecurityRulesApiClient { private readonly httpClient: HttpClient; private projectIdPrefix?: string; - constructor(private readonly app: FirebaseApp) { + constructor(private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseSecurityRulesError( 'invalid-argument', @@ -67,7 +68,7 @@ export class SecurityRulesApiClient { + 'instance.'); } - this.httpClient = new AuthorizedHttpClient(app); + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); } public getRuleset(name: string): Promise { diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index 9b624e03d9..bf18627a08 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -14,25 +14,48 @@ * limitations under the License. */ -import { FirebaseApp } from '../app/firebase-app'; +import { App } from '../app'; import * as validator from '../utils/validator'; import { SecurityRulesApiClient, RulesetResponse, RulesetContent, ListRulesetsResponse, } from './security-rules-api-client-internal'; import { FirebaseSecurityRulesError } from './security-rules-internal'; -import { securityRules } from './index'; -import RulesFile = securityRules.RulesFile; -import RulesetMetadata = securityRules.RulesetMetadata; -import RulesetMetadataList = securityRules.RulesetMetadataList; -import RulesetInterface = securityRules.Ruleset; -import SecurityRulesInterface = securityRules.SecurityRules; +/** + * A source file containing some Firebase security rules. The content includes raw + * source code including text formatting, indentation and comments. Use the + * [`securityRules.createRulesFileFromSource()`](securityRules.SecurityRules#createRulesFileFromSource) + * method to create new instances of this type. + */ +export interface RulesFile { + readonly name: string; + readonly content: string; +} -class RulesetMetadataListImpl implements RulesetMetadataList { +/** + * Required metadata associated with a ruleset. + */ +export interface RulesetMetadata { + /** + * Name of the `Ruleset` as a short string. This can be directly passed into APIs + * like {@link securityRules.SecurityRules.getRuleset `securityRules.getRuleset()`} + * and {@link securityRules.SecurityRules.deleteRuleset `securityRules.deleteRuleset()`}. + */ + readonly name: string; + /** + * Creation time of the `Ruleset` as a UTC timestamp string. + */ + readonly createTime: string; +} + +export class RulesetMetadataList { public readonly rulesets: RulesetMetadata[]; public readonly nextPageToken?: string; + /** + * @internal + */ constructor(response: ListRulesetsResponse) { if (!validator.isNonNullObject(response) || !validator.isArray(response.rulesets)) { throw new FirebaseSecurityRulesError( @@ -56,12 +79,15 @@ class RulesetMetadataListImpl implements RulesetMetadataList { /** * Represents a set of Firebase security rules. */ -export class Ruleset implements RulesetInterface { +export class Ruleset { public readonly name: string; public readonly createTime: string; public readonly source: RulesFile[]; + /** + * @internal + */ constructor(ruleset: RulesetResponse) { if (!validator.isNonNullObject(ruleset) || !validator.isNonEmptyString(ruleset.name) || @@ -84,7 +110,7 @@ export class Ruleset implements RulesetInterface { * Do not call this constructor directly. Instead, use * [`admin.securityRules()`](securityRules#securityRules). */ -export class SecurityRules implements SecurityRulesInterface { +export class SecurityRules { private static readonly CLOUD_FIRESTORE = 'cloud.firestore'; private static readonly FIREBASE_STORAGE = 'firebase.storage'; @@ -92,10 +118,11 @@ export class SecurityRules implements SecurityRulesInterface { private readonly client: SecurityRulesApiClient; /** - * @param {object} app The app for this SecurityRules service. + * @param app The app for this SecurityRules service. * @constructor + * @internal */ - constructor(readonly app: FirebaseApp) { + constructor(readonly app: App) { this.client = new SecurityRulesApiClient(app); } @@ -302,7 +329,7 @@ export class SecurityRules implements SecurityRulesInterface { public listRulesetMetadata(pageSize = 100, nextPageToken?: string): Promise { return this.client.listRulesets(pageSize, nextPageToken) .then((response) => { - return new RulesetMetadataListImpl(response); + return new RulesetMetadataList(response); }); } diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index d5107b9275..2a07381b09 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -73,6 +73,7 @@ import './project-management/android-app.spec'; import './project-management/ios-app.spec'; // SecurityRules +import './security-rules/index.spec'; import './security-rules/security-rules.spec'; import './security-rules/security-rules-api-client.spec'; diff --git a/test/unit/security-rules/index.spec.ts b/test/unit/security-rules/index.spec.ts new file mode 100644 index 0000000000..ad6a0b05de --- /dev/null +++ b/test/unit/security-rules/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 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 chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getSecurityRules, SecurityRules } from '../../../src/security-rules/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('SecurityRules', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID. Initialize the SDK ' + + 'with service account credentials, or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getSecurityRules()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getSecurityRules(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const rules = getSecurityRules(mockCredentialApp); + return rules.getFirestoreRuleset() + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getSecurityRules(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const rules1: SecurityRules = getSecurityRules(mockApp); + const rules2: SecurityRules = getSecurityRules(mockApp); + expect(rules1).to.equal(rules2); + }); + }); +}); diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index 426d46c6ae..70611d7db5 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -19,7 +19,7 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { SecurityRules } from '../../../src/security-rules/security-rules'; +import { SecurityRules } from '../../../src/security-rules/index'; import { FirebaseApp } from '../../../src/app/firebase-app'; import * as mocks from '../../resources/mocks'; import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client-internal';