diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index dacfadc60d..e7c1a78545 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -69,6 +69,7 @@ export class Ruleset implements RulesetMetadata { export class SecurityRules implements FirebaseServiceInterface { private static readonly CLOUD_FIRESTORE = 'cloud.firestore'; + private static readonly FIREBASE_STORAGE = 'firebase.storage'; public readonly INTERNAL = new SecurityRulesInternals(); @@ -126,6 +127,24 @@ export class SecurityRules implements FirebaseServiceInterface { return this.releaseRuleset(ruleset, SecurityRules.CLOUD_FIRESTORE); } + /** + * Gets the Ruleset currently applied to a Cloud Storage bucket. Rejects with a `not-found` error if no Ruleset is + * applied on the bucket. + * + * @param {string=} bucket Optional name of the Cloud Storage bucket to be retrieved. If name is not specified, + * retrieves the ruleset applied on the default bucket configured via `AppOptions`. + * @returns {Promise} A promise that fulfills with the Cloud Storage Ruleset. + */ + public getStorageRuleset(bucket?: string): Promise { + return Promise.resolve() + .then(() => { + return this.getBucketName(bucket); + }) + .then((bucketName) => { + return this.getRulesetForRelease(`${SecurityRules.FIREBASE_STORAGE}/${bucketName}`); + }); + } + /** * Creates a `RulesFile` with the given name and source. Throws if any of the arguments are invalid. This is a * local operation, and does not involve any network API calls. @@ -214,6 +233,20 @@ export class SecurityRules implements FirebaseServiceInterface { return; }); } + + private getBucketName(bucket?: string): string { + const bucketName = (typeof bucket !== 'undefined') ? bucket : this.app.options.storageBucket; + if (!validator.isNonEmptyString(bucketName)) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', + 'Bucket name not specified or invalid. Specify a default bucket name via the ' + + 'storageBucket option when initializing the app, or specify the bucket name ' + + 'explicitly when calling the rules API.', + ); + } + + return bucketName; + } } class SecurityRulesInternals implements FirebaseServiceInternalsInterface { diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index 9bda4684e5..f3d024f8d3 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -231,6 +231,97 @@ describe('SecurityRules', () => { const file = ruleset.source[0]; expect(file.name).equals('firestore.rules'); expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(getRelease).to.have.been.calledOnce.and.calledWith( + 'cloud.firestore'); + }); + }); + }); + + describe('getStorageRuleset', () => { + const invalidBucketNames: any[] = [null, '', true, false, 1, 0, {}, []]; + const invalidBucketError = new FirebaseSecurityRulesError( + 'invalid-argument', + 'Bucket name not specified or invalid. Specify a default bucket name via the ' + + 'storageBucket option when initializing the app, or specify the bucket name ' + + 'explicitly when calling the rules API.', + ); + invalidBucketNames.forEach((bucketName) => { + it(`should reject when called with: ${JSON.stringify(bucketName)}`, () => { + return securityRules.getStorageRuleset(bucketName) + .should.eventually.be.rejected.and.deep.equal(invalidBucketError); + }); + }); + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.getStorageRuleset() + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should reject when getRelease response is invalid', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .resolves({}); + stubs.push(stub); + + return securityRules.getStorageRuleset() + .should.eventually.be.rejected.and.have.property( + 'message', 'Ruleset name not found for firebase.storage/bucketName.appspot.com.'); + }); + + it('should resolve with Ruleset for the default bucket on success', () => { + const getRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .resolves({ + rulesetName: 'projects/test-project/rulesets/foo', + }); + const getRuleset = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + stubs.push(getRelease, getRuleset); + + return securityRules.getStorageRuleset() + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(getRelease).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/bucketName.appspot.com'); + }); + }); + + it('should resolve with Ruleset for the specified bucket on success', () => { + const getRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .resolves({ + rulesetName: 'projects/test-project/rulesets/foo', + }); + const getRuleset = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + stubs.push(getRelease, getRuleset); + + return securityRules.getStorageRuleset('other.appspot.com') + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(getRelease).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/other.appspot.com'); }); }); });