diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index 49196fc5f0..8d34befda4 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -178,6 +178,20 @@ toc: - title: "ShaCertificate" path: /docs/reference/admin/node/admin.projectManagement.ShaCertificate +- title: "admin.securityRules" + path: /docs/reference/admin/node/admin.securityRules + section: + - title: "RulesFile" + path: /docs/reference/admin/node/admin.securityRules.RulesFile + - title: "Ruleset" + path: /docs/reference/admin/node/admin.securityRules.Ruleset + - title: "RulesetMetadata" + path: /docs/reference/admin/node/admin.securityRules.RulesetMetadata + - title: "RulesetMetadataList" + path: /docs/reference/admin/node/admin.securityRules.RulesetMetadataList + - title: "SecurityRules" + path: /docs/reference/admin/node/admin.securityRules.SecurityRules + - title: "admin.storage" path: /docs/reference/admin/node/admin.storage section: diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 39a26bb419..1b21aefa8b 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -30,6 +30,7 @@ import {Firestore} from '@google-cloud/firestore'; import {FirestoreService} from './firestore/firestore'; import {InstanceId} from './instance-id/instance-id'; import {ProjectManagement} from './project-management/project-management'; +import {SecurityRules} from './security-rules/security-rules'; import {Agent} from 'http'; @@ -366,6 +367,19 @@ export class FirebaseApp { }); } + /** + * Returns the SecurityRules service instance associated with this app. + * + * @return {SecurityRules} 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); + }); + } + /** * Returns the name of the FirebaseApp instance. * diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index cbe4527252..86ee91c991 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -34,6 +34,7 @@ import {Database} from '@firebase/database'; import {Firestore} from '@google-cloud/firestore'; import {InstanceId} from './instance-id/instance-id'; import {ProjectManagement} from './project-management/project-management'; +import { SecurityRules } from './security-rules/security-rules'; import * as validator from './utils/validator'; @@ -419,6 +420,18 @@ export class FirebaseNamespace { return Object.assign(fn, {ProjectManagement: projectManagement}); } + /** + * Gets the `SecurityRules` service namespace. The returned namespace can be used to get the + * `SecurityRules` service for the default app or an explicitly specified app. + */ + get securityRules(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + return this.ensureApp(app).securityRules(); + }; + const securityRules = require('./security-rules/security-rules').SecurityRules; + return Object.assign(fn, {SecurityRules: securityRules}); + } + /** * Initializes the FirebaseApp instance. * diff --git a/src/index.d.ts b/src/index.d.ts index 616a5fa204..216f7de9f1 100755 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -363,6 +363,37 @@ declare namespace admin { * `ProjectManagement` service associated with the provided app. */ function projectManagement(app?: admin.app.App): admin.projectManagement.ProjectManagement; + + /** + * Gets the {@link admin.securityRules.SecurityRules + * `SecurityRules`} service for the default app or a given app. + * + * `admin.securityRules()` can be called with no arguments to access the + * default app's {@link admin.securityRules.SecurityRules + * `SecurityRules`} service, or as `admin.securityRules(app)` to access + * the {@link admin.securityRules.SecurityRules `SecurityRules`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the SecurityRules service for the default app + * var defaultSecurityRules = admin.securityRules(); + * ``` + * + * @example + * ```javascript + * // Get the SecurityRules service for a given app + * var otherSecurityRules = admin.securityRules(otherApp); + * ``` + * + * @param app Optional app to return the `SecurityRules` service + * for. If not provided, the default `SecurityRules` service will + * be returned. + * @return The default `SecurityRules` service if no app is provided, or the + * `SecurityRules` service associated with the provided app. + */ + function securityRules(app?: admin.app.App): admin.securityRules.SecurityRules; + function initializeApp(options?: admin.AppOptions, name?: string): admin.app.App; } @@ -423,6 +454,7 @@ declare namespace admin.app { instanceId(): admin.instanceId.InstanceId; messaging(): admin.messaging.Messaging; projectManagement(): admin.projectManagement.ProjectManagement; + securityRules(): admin.securityRules.SecurityRules; storage(): admin.storage.Storage; /** @@ -5169,6 +5201,196 @@ declare namespace admin.projectManagement { } } +declare namespace admin.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()`](admin.securityRules.SecurityRules#createRulesFileFromSource) + * method to create new instances of this type. + */ + interface RulesFile { + readonly name: string; + readonly content: string; + } + + /** + * Required metadata associated with a ruleset. + */ + interface RulesetMetadata { + /** + * Name of the `Ruleset` as a short string. This can be directly passed into APIs + * like [`securityRules.getRuleset()`](admin.securityRules.SecurityRules#getRuleset) + * and [`securityRules.deleteRuleset()`](admin.securityRules.SecurityRules#deleteRuleset). + */ + readonly name: string; + + /** + * Creation time of the `Ruleset` as a UTC timestamp string. + */ + readonly createTime: string; + } + + /** + * A set of Firebase security rules. + */ + interface Ruleset extends RulesetMetadata { + readonly source: RulesFile[]; + } + + 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; + } + + /** + * The Firebase `SecurityRules` service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.securityRules()`](admin.securityRules#securityRules). + */ + interface SecurityRules { + app: admin.app.App; + + /** + * Creates a {@link admin.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 admin.securityRules.Ruleset `Ruleset`} from the given + * {@link admin.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 admin.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 admin.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 a page of rulesets. + */ + listRulesetMetadata( + pageSize?: number, nextPageToken?: string): Promise; + + /** + * Gets the {@link admin.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 admin.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 admin.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 admin.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 admin.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 admin.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 admin.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 admin.AppOptions `AppOptions`}. + * @return A promise that fulfills when the ruleset is released. + */ + releaseStorageRuleset( + ruleset: string | RulesetMetadata, bucket?: string): Promise; + } +} + declare module 'firebase-admin' { } diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index 7475636ffd..36c53813da 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -40,6 +40,7 @@ import {Firestore} from '@google-cloud/firestore'; import {Database} from '@firebase/database'; import {InstanceId} from '../../src/instance-id/instance-id'; import {ProjectManagement} from '../../src/project-management/project-management'; +import { SecurityRules } from '../../src/security-rules/security-rules'; import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error'; chai.should(); @@ -584,6 +585,32 @@ describe('FirebaseApp', () => { }); }); + describe('securityRules()', () => { + it('should throw if the app has already been deleted', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + return app.delete().then(() => { + expect(() => { + return app.securityRules(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); + + it('should return the securityRules client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + const securityRules: SecurityRules = app.securityRules(); + expect(securityRules).to.not.be.null; + }); + + it('should return a cached version of SecurityRules on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: SecurityRules = app.securityRules(); + const service2: SecurityRules = app.securityRules(); + expect(service1).to.equal(service2); + }); + }); + describe('#[service]()', () => { it('should throw if the app has already been deleted', () => { firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory); diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 370b3de0d0..feb8422091 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -49,6 +49,7 @@ import { } from '@google-cloud/firestore'; import {InstanceId} from '../../src/instance-id/instance-id'; import {ProjectManagement} from '../../src/project-management/project-management'; +import { SecurityRules } from '../../src/security-rules/security-rules'; chai.should(); chai.use(sinonChai); @@ -624,4 +625,38 @@ describe('FirebaseNamespace', () => { .to.be.deep.equal(ProjectManagement); }); }); + + describe('#securityRules()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.securityRules(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.securityRules(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should return a valid namespace when the default app is initialized', () => { + const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const securityRules: SecurityRules = firebaseNamespace.securityRules(); + expect(securityRules).to.not.be.null; + expect(securityRules.app).to.be.deep.equal(app); + }); + + it('should return a valid namespace when the named app is initialized', () => { + const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const securityRules: SecurityRules = firebaseNamespace.securityRules(app); + expect(securityRules).to.not.be.null; + expect(securityRules.app).to.be.deep.equal(app); + }); + + it('should return a reference to SecurityRules type', () => { + expect(firebaseNamespace.securityRules.SecurityRules) + .to.be.deep.equal(SecurityRules); + }); + }); });