From 53c34c23673b08c82089306383d1cec89f313d57 Mon Sep 17 00:00:00 2001 From: Erick Daniszewski Date: Fri, 26 Jun 2020 07:36:15 -0400 Subject: [PATCH 1/4] fix: update the method by which IAM roles are applied * removes the previous implementation using deploymentmanager, as it does not appear to work as documented * adds support for setting IAM via cloudfunctions API * adds test cases --- deploy/googleDeploy.js | 7 +- deploy/googleDeploy.test.js | 4 + deploy/lib/setIamPolicy.js | 69 ++++ deploy/lib/setIamPolicy.test.js | 192 +++++++++ package/lib/compileFunctions.js | 65 ++- package/lib/compileFunctions.test.js | 572 +++++++++++++++++++++++++++ package/lib/prepareDeployment.js | 4 + 7 files changed, 909 insertions(+), 4 deletions(-) create mode 100644 deploy/lib/setIamPolicy.js create mode 100644 deploy/lib/setIamPolicy.test.js diff --git a/deploy/googleDeploy.js b/deploy/googleDeploy.js index 98360fa..f3adcdc 100644 --- a/deploy/googleDeploy.js +++ b/deploy/googleDeploy.js @@ -10,6 +10,7 @@ const monitorDeployment = require('../shared/monitorDeployment'); const uploadArtifacts = require('./lib/uploadArtifacts'); const updateDeployment = require('./lib/updateDeployment'); const cleanupDeploymentBucket = require('./lib/cleanupDeploymentBucket'); +const setIamPolicy = require('./lib/setIamPolicy'); class GoogleDeploy { constructor(serverless, options) { @@ -26,7 +27,8 @@ class GoogleDeploy { monitorDeployment, uploadArtifacts, updateDeployment, - cleanupDeploymentBucket + cleanupDeploymentBucket, + setIamPolicy ); this.hooks = { @@ -37,7 +39,8 @@ class GoogleDeploy { .then(this.createDeployment) .then(this.setDeploymentBucketName) .then(this.uploadArtifacts) - .then(this.updateDeployment), + .then(this.updateDeployment) + .then(this.setIamPolicy), 'after:deploy:deploy': () => BbPromise.bind(this).then(this.cleanupDeploymentBucket), }; diff --git a/deploy/googleDeploy.test.js b/deploy/googleDeploy.test.js index 81acebd..eb63c25 100644 --- a/deploy/googleDeploy.test.js +++ b/deploy/googleDeploy.test.js @@ -43,6 +43,7 @@ describe('GoogleDeploy', () => { let uploadArtifactsStub; let updateDeploymentStub; let cleanupDeploymentBucketStub; + let setIamPolicyStub; beforeEach(() => { validateStub = sinon.stub(googleDeploy, 'validate').returns(BbPromise.resolve()); @@ -62,6 +63,7 @@ describe('GoogleDeploy', () => { cleanupDeploymentBucketStub = sinon .stub(googleDeploy, 'cleanupDeploymentBucket') .returns(BbPromise.resolve()); + setIamPolicyStub = sinon.stub(googleDeploy, 'setIamPolicy').returns(BbPromise.resolve()); }); afterEach(() => { @@ -72,6 +74,7 @@ describe('GoogleDeploy', () => { googleDeploy.uploadArtifacts.restore(); googleDeploy.updateDeployment.restore(); googleDeploy.cleanupDeploymentBucket.restore(); + googleDeploy.setIamPolicy.restore(); }); it('should run "before:deploy:deploy" promise chain', () => @@ -86,6 +89,7 @@ describe('GoogleDeploy', () => { expect(setDeploymentBucketNameStub.calledAfter(createDeploymentStub)).toEqual(true); expect(uploadArtifactsStub.calledAfter(createDeploymentStub)).toEqual(true); expect(updateDeploymentStub.calledAfter(uploadArtifactsStub)).toEqual(true); + expect(setIamPolicyStub.calledAfter(updateDeploymentStub)).toEqual(true); })); it('should run "after:deploy:deploy" promise chain', () => diff --git a/deploy/lib/setIamPolicy.js b/deploy/lib/setIamPolicy.js new file mode 100644 index 0000000..c584ed2 --- /dev/null +++ b/deploy/lib/setIamPolicy.js @@ -0,0 +1,69 @@ +'use strict'; + +const _ = require('lodash'); +const BbPromise = require('bluebird'); + +module.exports = { + setIamPolicy() { + return BbPromise.bind(this).then(this.getFunctions).then(this.setPolicies); + }, + + getFunctions() { + const project = this.serverless.service.provider.project; + const region = this.options.region; + + const params = { + parent: `projects/${project}/locations/${region}`, + }; + + return this.provider + .request('cloudfunctions', 'projects', 'locations', 'functions', 'list', params) + .then((response) => { + return response.functions; + }); + }, + + setPolicies(functions) { + const policies = this.serverless.service.provider.functionIamBindings; + + // If there are no IAM policies configured with any function, there is nothing to + // do here. + if (!policies || !Object.keys(policies).length) { + return BbPromise.resolve(); + } + this.serverless.cli.log('Setting IAM policies...'); + + _.forEach(policies, (value, key) => { + const func = functions.find((fn) => { + return fn.name === key; + }); + if (func) { + const params = { + resource: func.name, + requestBody: { + policy: { + bindings: value, + }, + }, + }; + + this.provider.request( + 'cloudfunctions', + 'projects', + 'locations', + 'functions', + 'setIamPolicy', + params + ); + } else { + const errorMessage = [ + `Unable to set IAM bindings (${value}) for "${key}": function not found for`, + ` project "${this.serverless.service.provider.project}" in region "${this.options.region}".`, + ].join(''); + throw new Error(errorMessage); + } + }); + + return BbPromise.resolve(); + }, +}; diff --git a/deploy/lib/setIamPolicy.test.js b/deploy/lib/setIamPolicy.test.js new file mode 100644 index 0000000..d8e6d96 --- /dev/null +++ b/deploy/lib/setIamPolicy.test.js @@ -0,0 +1,192 @@ +'use strict'; + +const sinon = require('sinon'); +const BbPromise = require('bluebird'); + +const GoogleProvider = require('../../provider/googleProvider'); +const GoogleDeploy = require('../googleDeploy'); +const Serverless = require('../../test/serverless'); + +describe('SetIamPolicy', () => { + let serverless; + let googleDeploy; + let requestStub; + + beforeEach(() => { + serverless = new Serverless(); + serverless.service.service = 'my-service'; + serverless.service.provider = { + project: 'my-project', + }; + serverless.config = { + servicePath: 'tmp', + }; + serverless.setProvider('google', new GoogleProvider(serverless)); + const options = { + stage: 'dev', + region: 'us-central1', + }; + googleDeploy = new GoogleDeploy(serverless, options); + requestStub = sinon.stub(googleDeploy.provider, 'request'); + }); + + afterEach(() => { + googleDeploy.provider.request.restore(); + }); + + describe('#setIamPolicy', () => { + let getFunctionsStub; + let setPoliciesStub; + + beforeEach(() => { + getFunctionsStub = sinon.stub(googleDeploy, 'getFunctions').returns(BbPromise.resolve()); + setPoliciesStub = sinon.stub(googleDeploy, 'setPolicies').returns(BbPromise.resolve()); + }); + + afterEach(() => { + googleDeploy.getFunctions.restore(); + googleDeploy.setPolicies.restore(); + }); + + it('should run the promise chain', () => { + googleDeploy.setIamPolicy().then(() => { + expect(getFunctionsStub.calledOnce).toEqual(true); + expect(setPoliciesStub.calledAfter(getFunctionsStub)); + }); + }); + }); + + describe('#getFunctions', () => { + it('should return "undefined" if no functions are found', () => { + requestStub.returns(BbPromise.resolve([])); + + return googleDeploy.getFunctions().then((foundFunctions) => { + expect(foundFunctions).toEqual(undefined); + expect( + requestStub.calledWithExactly( + 'cloudfunctions', + 'projects', + 'locations', + 'functions', + 'list', + { + parent: 'projects/my-project/locations/us-central1', + } + ) + ).toEqual(true); + }); + }); + + it('should return all functions that are found', () => { + const response = { + functions: [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }], + }; + requestStub.returns(BbPromise.resolve(response)); + + return googleDeploy.getFunctions().then((foundFunctions) => { + expect(foundFunctions).toEqual([ + { name: 'cloud-function-1' }, + { name: 'cloud-function-2' }, + ]); + expect( + requestStub.calledWithExactly( + 'cloudfunctions', + 'projects', + 'locations', + 'functions', + 'list', + { + parent: 'projects/my-project/locations/us-central1', + } + ) + ).toEqual(true); + }); + }); + }); + + describe('#setPolicies', () => { + let consoleLogStub; + + beforeEach(() => { + consoleLogStub = sinon.stub(googleDeploy.serverless.cli, 'log').returns(); + googleDeploy.serverless.service.provider.functionIamBindings = {}; + }); + + afterEach(() => { + googleDeploy.serverless.cli.log.restore(); + }); + + it('should resolve if functionIamBindings is undefined', () => { + const foundFunctions = [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }]; + delete googleDeploy.serverless.service.provider.functionIamBindings; + + return googleDeploy.setPolicies(foundFunctions).then(() => { + expect(consoleLogStub.calledOnce).toEqual(false); + }); + }); + + it('should resolve if there are no IAM policies configured', () => { + const foundFunctions = [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }]; + + return googleDeploy.setPolicies(foundFunctions).then(() => { + expect(consoleLogStub.calledOnce).toEqual(false); + }); + }); + + it('should error if there are no existing functions to apply configured IAM to', () => { + const foundFunctions = []; + googleDeploy.serverless.service.provider.functionIamBindings = { + 'cloud-function-1': [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }], + }; + + expect(() => googleDeploy.setPolicies(foundFunctions)).toThrow(Error); + expect(consoleLogStub.calledOnce).toEqual(true); + expect(requestStub.calledOnce).toEqual(false); + }); + + it('should error if a configured function is not found', () => { + const foundFunctions = [{ name: 'cloud-function-2' }]; + googleDeploy.serverless.service.provider.functionIamBindings = { + 'cloud-function-1': [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }], + }; + + expect(() => googleDeploy.setPolicies(foundFunctions)).toThrow(Error); + expect(consoleLogStub.calledOnce).toEqual(true); + expect(requestStub.calledOnce).toEqual(false); + }); + + it('should set the IAM policy for the configured functions', () => { + const foundFunctions = [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }]; + googleDeploy.serverless.service.provider.functionIamBindings = { + 'cloud-function-2': [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }], + }; + requestStub.returns(BbPromise.resolve()); + + return googleDeploy.setPolicies(foundFunctions).then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + requestStub.calledWithExactly( + 'cloudfunctions', + 'projects', + 'locations', + 'functions', + 'setIamPolicy', + { + resource: 'cloud-function-2', + requestBody: { + policy: { + bindings: [ + { + role: 'roles/cloudfunctions.invoker', + members: ['allUsers'], + }, + ], + }, + }, + } + ) + ).toEqual(true); + }); + }); + }); +}); diff --git a/package/lib/compileFunctions.js b/package/lib/compileFunctions.js index c9dd256..f84fca7 100644 --- a/package/lib/compileFunctions.js +++ b/package/lib/compileFunctions.js @@ -24,6 +24,9 @@ module.exports = { validateHandlerProperty(funcObject, functionName); validateEventsProperty(funcObject, functionName); validateVpcConnectorProperty(funcObject, functionName); + validateIamProperty(funcObject, functionName); + + const eventType = Object.keys(funcObject.events[0])[0]; const funcTemplate = getFunctionTemplate( funcObject, @@ -52,6 +55,28 @@ module.exports = { funcObject.environment // eslint-disable-line comma-dangle ); + const allowUnauthenticated = + eventType === 'http' && + (funcObject.allowUnauthenticated || this.serverless.service.provider.allowUnauthenticated); + + // Collect the configured IAM bindings at the function and provider level and merge the + // members for each defined role. This transforms the array of IAM bindings into a mapping + // in order to easily merge. + const iamBindings = _.reduce( + _.concat( + _.get(funcObject, 'iam.bindings') || [], + _.get(this, 'serverless.service.provider.iam.bindings') || [], + allowUnauthenticated + ? [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }] + : [] + ), + (result, value) => { + result[value.role] = _.union(result[value.role] || [], value.members); + return result; + }, + {} + ); + if (!funcTemplate.properties.serviceAccountEmail) { delete funcTemplate.properties.serviceAccountEmail; } @@ -76,8 +101,6 @@ module.exports = { _.get(funcObject, 'labels') || {} // eslint-disable-line comma-dangle ); - const eventType = Object.keys(funcObject.events[0])[0]; - if (eventType === 'http') { const url = funcObject.events[0].http; @@ -96,6 +119,16 @@ module.exports = { } this.serverless.service.provider.compiledConfigurationTemplate.resources.push(funcTemplate); + + if (Object.keys(iamBindings).length) { + const fnResource = `projects/${projectName}/locations/${this.serverless.service.provider.region}/functions/${funcObject.name}`; + this.serverless.service.provider.functionIamBindings[fnResource] = _.flatMap( + iamBindings, + (value, key) => { + return { role: key, members: value }; + } + ); + } }); return BbPromise.resolve(); @@ -157,6 +190,29 @@ const validateVpcConnectorProperty = (funcObject, functionName) => { } }; +const validateIamProperty = (funcObject, functionName) => { + if (_.get(funcObject, 'iam.bindings') && funcObject.iam.bindings.length > 0) { + funcObject.iam.bindings.forEach((binding) => { + if (!binding.role) { + const errorMessage = [ + `The function "${functionName}" has no role specified for an IAM binding.`, + ' Each binding requires a role. For details on supported roles, see the documentation', + ' at: https://cloud.google.com/iam/docs/understanding-roles', + ].join(''); + throw new Error(errorMessage); + } + if (!Array.isArray(binding.members) || !binding.members.length) { + const errorMessage = [ + `The function "${functionName}" has no members specified for an IAM binding.`, + ' Each binding requires at least one member to be assigned. See the IAM documentation', + ' for details on configuring members: https://cloud.google.com/iam/docs/overview', + ].join(''); + throw new Error(errorMessage); + } + }); + } +}; + const getFunctionTemplate = (funcObject, projectName, region, sourceArchiveUrl) => { //eslint-disable-line return { @@ -171,5 +227,10 @@ const getFunctionTemplate = (funcObject, projectName, region, sourceArchiveUrl) function: funcObject.name, sourceArchiveUrl, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }; }; diff --git a/package/lib/compileFunctions.test.js b/package/lib/compileFunctions.test.js index a33e771..c5c6044 100644 --- a/package/lib/compileFunctions.test.js +++ b/package/lib/compileFunctions.test.js @@ -32,6 +32,7 @@ describe('CompileFunctions', () => { }; googlePackage = new GooglePackage(serverless, options); consoleLogStub = sinon.stub(googlePackage.serverless.cli, 'log').returns(); + serverless.service.provider.functionIamBindings = {}; }); afterEach(() => { @@ -128,6 +129,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -165,6 +167,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -202,6 +205,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -239,6 +243,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -280,6 +285,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -321,6 +327,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -367,6 +374,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -409,6 +417,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -451,6 +460,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -504,6 +514,7 @@ describe('CompileFunctions', () => { TEST_VAR: 'test', TEST_FOO: 'foo', }); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -540,6 +551,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -615,6 +627,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -655,6 +668,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -697,6 +711,7 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); }); }); @@ -764,6 +779,563 @@ describe('CompileFunctions', () => { expect( googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); + }); + }); + + it('should throw an error if an IAM policy binding has no role', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + iam: { + bindings: [ + { + members: ['foobar'], + }, + ], + }, + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should throw an error if an IAM policy binding has no members defined', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + iam: { + bindings: [ + { + role: 'foobar', + }, + ], + }, + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should throw an error if an IAM policy binding has 0 members', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + iam: { + bindings: [ + { + role: 'foobar', + members: [], + }, + ], + }, + }, + }; + + expect(() => googlePackage.compileFunctions()).toThrow(Error); + }); + + it('should add the cloudfunctions.invoker role for allUsers when allowUnauthenticated is set on a function for "http" event', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + allowUnauthenticated: true, + }, + }; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + labels: {}, + }, + }, + ]; + const functionIamBindings = { + 'projects/myProject/locations/us-central1/functions/my-service-dev-func1': [ + { + role: 'roles/cloudfunctions.invoker', + members: ['allUsers'], + }, + ], + }; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual( + functionIamBindings + ); + }); + }); + + it('should add the cloudfunctions.invoker role for allUsers when allowUnauthenticated is set on the provider for "http" event', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + }, + }; + googlePackage.serverless.service.provider.allowUnauthenticated = true; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + labels: {}, + }, + }, + ]; + const functionIamBindings = { + 'projects/myProject/locations/us-central1/functions/my-service-dev-func1': [ + { + role: 'roles/cloudfunctions.invoker', + members: ['allUsers'], + }, + ], + }; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual( + functionIamBindings + ); + }); + }); + + it('should not add the cloudfunctions.invoker role for allUsers when allowUnauthenticated is set on a function for "event" event', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { + event: { + eventType: 'foo', + resource: 'some-resource', + }, + }, + ], + allowUnauthenticated: true, + }, + }; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + eventTrigger: { + eventType: 'foo', + resource: 'some-resource', + }, + labels: {}, + }, + }, + ]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); + }); + }); + + it('should not add the cloudfunctions.invoker role for allUsers when allowUnauthenticated is set on the provider for "event" event', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [ + { + event: { + eventType: 'foo', + resource: 'some-resource', + }, + }, + ], + }, + }; + googlePackage.serverless.service.provider.allowUnauthenticated = true; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + eventTrigger: { + eventType: 'foo', + resource: 'some-resource', + }, + labels: {}, + }, + }, + ]; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual({}); + }); + }); + + it('should add the custom IAM policy bindings based on function configuration', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + iam: { + bindings: [ + { + role: 'roles/foobar', + members: ['some-user'], + }, + ], + }, + }, + }; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + labels: {}, + }, + }, + ]; + const functionIamBindings = { + 'projects/myProject/locations/us-central1/functions/my-service-dev-func1': [ + { + role: 'roles/foobar', + members: ['some-user'], + }, + ], + }; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual( + functionIamBindings + ); + }); + }); + + it('should add the custom IAM policy bindings based on the provider configuration', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + }, + }; + googlePackage.serverless.service.provider.iam = { + bindings: [ + { + role: 'roles/foobar', + members: ['some-user'], + }, + ], + }; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + labels: {}, + }, + }, + ]; + const functionIamBindings = { + 'projects/myProject/locations/us-central1/functions/my-service-dev-func1': [ + { + role: 'roles/foobar', + members: ['some-user'], + }, + ], + }; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual( + functionIamBindings + ); + }); + }); + + it('should add the custom IAM policy bindings based on the merged provider and function configuration', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + iam: { + bindings: [ + { + role: 'role2', + members: ['user1'], + }, + { + role: 'role3', + members: ['user4', 'user2'], + }, + ], + }, + }, + }; + googlePackage.serverless.service.provider.iam = { + bindings: [ + { + role: 'role1', + members: ['user1'], + }, + { + role: 'role2', + members: ['user1', 'user2', 'user3'], + }, + ], + }; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + labels: {}, + }, + }, + ]; + const functionIamBindings = { + 'projects/myProject/locations/us-central1/functions/my-service-dev-func1': [ + { + role: 'role2', + members: ['user1', 'user2', 'user3'], + }, + { + role: 'role3', + members: ['user4', 'user2'], + }, + { + role: 'role1', + members: ['user1'], + }, + ], + }; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual( + functionIamBindings + ); + }); + }); + + it('should merge the allowUnauthenticated binding with custom bindings', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + allowUnauthenticated: true, + iam: { + bindings: [ + { + role: 'role1', + members: ['user1'], + }, + ], + }, + }, + }; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + labels: {}, + }, + }, + ]; + const functionIamBindings = { + 'projects/myProject/locations/us-central1/functions/my-service-dev-func1': [ + { + role: 'role1', + members: ['user1'], + }, + { + role: 'roles/cloudfunctions.invoker', + members: ['allUsers'], + }, + ], + }; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual( + functionIamBindings + ); + }); + }); + + it('should merge the allowUnauthenticated binding with custom bindings with same role', () => { + googlePackage.serverless.service.functions = { + func1: { + handler: 'func1', + events: [{ http: 'foo' }], + allowUnauthenticated: true, + iam: { + bindings: [ + { + role: 'role1', + members: ['user1'], + }, + { + role: 'roles/cloudfunctions.invoker', + members: ['user1', 'user2'], + }, + ], + }, + }, + }; + + const compiledResources = [ + { + type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', + name: 'my-service-dev-func1', + properties: { + parent: 'projects/myProject/locations/us-central1', + runtime: 'nodejs8', + function: 'my-service-dev-func1', + entryPoint: 'func1', + availableMemoryMb: 256, + timeout: '60s', + sourceArchiveUrl: 'gs://sls-my-service-dev-12345678/some-path/artifact.zip', + httpsTrigger: { + url: 'foo', + }, + labels: {}, + }, + }, + ]; + const functionIamBindings = { + 'projects/myProject/locations/us-central1/functions/my-service-dev-func1': [ + { + role: 'role1', + members: ['user1'], + }, + { + role: 'roles/cloudfunctions.invoker', + members: ['user1', 'user2', 'allUsers'], + }, + ], + }; + + return googlePackage.compileFunctions().then(() => { + expect(consoleLogStub.calledOnce).toEqual(true); + expect( + googlePackage.serverless.service.provider.compiledConfigurationTemplate.resources + ).toEqual(compiledResources); + expect(googlePackage.serverless.service.provider.functionIamBindings).toEqual( + functionIamBindings + ); }); }); }); diff --git a/package/lib/prepareDeployment.js b/package/lib/prepareDeployment.js index 2287807..a5fdc8e 100644 --- a/package/lib/prepareDeployment.js +++ b/package/lib/prepareDeployment.js @@ -27,6 +27,10 @@ module.exports = { this.serverless.service.provider.compiledConfigurationTemplate = deploymentTemplate; + // functionIamBindings holds a mapping of function name to array of bindings which + // were configured for it. This is populated during function compile. + this.serverless.service.provider.functionIamBindings = {}; + return BbPromise.resolve(); }, }; From 23dd8df40d8a76e340e212edbe6dfc513aeda037 Mon Sep 17 00:00:00 2001 From: Erick Daniszewski Date: Thu, 2 Jul 2020 08:18:10 -0400 Subject: [PATCH 2/4] review updates --- deploy/lib/setIamPolicy.js | 31 +++++++++++++++++-------------- package/lib/compileFunctions.js | 13 +++++-------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/deploy/lib/setIamPolicy.js b/deploy/lib/setIamPolicy.js index c584ed2..d2bf7cc 100644 --- a/deploy/lib/setIamPolicy.js +++ b/deploy/lib/setIamPolicy.js @@ -1,7 +1,7 @@ 'use strict'; -const _ = require('lodash'); const BbPromise = require('bluebird'); +const ServerlessError = require('serverless/lib/classes/Error').ServerlessError; module.exports = { setIamPolicy() { @@ -33,37 +33,40 @@ module.exports = { } this.serverless.cli.log('Setting IAM policies...'); - _.forEach(policies, (value, key) => { + const promises = []; + Object.entries(policies).forEach((entry) => { const func = functions.find((fn) => { - return fn.name === key; + return fn.name === entry[0]; }); if (func) { const params = { resource: func.name, requestBody: { policy: { - bindings: value, + bindings: entry[1], }, }, }; - this.provider.request( - 'cloudfunctions', - 'projects', - 'locations', - 'functions', - 'setIamPolicy', - params + promises.push( + this.provider.request( + 'cloudfunctions', + 'projects', + 'locations', + 'functions', + 'setIamPolicy', + params + ) ); } else { const errorMessage = [ - `Unable to set IAM bindings (${value}) for "${key}": function not found for`, + `Unable to set IAM bindings (${entry[1]}) for "${entry[0]}": function not found for`, ` project "${this.serverless.service.provider.project}" in region "${this.options.region}".`, ].join(''); - throw new Error(errorMessage); + throw new ServerlessError(errorMessage); } }); - return BbPromise.resolve(); + return BbPromise.all(promises); }, }; diff --git a/package/lib/compileFunctions.js b/package/lib/compileFunctions.js index f84fca7..dce977e 100644 --- a/package/lib/compileFunctions.js +++ b/package/lib/compileFunctions.js @@ -62,20 +62,17 @@ module.exports = { // Collect the configured IAM bindings at the function and provider level and merge the // members for each defined role. This transforms the array of IAM bindings into a mapping // in order to easily merge. - const iamBindings = _.reduce( - _.concat( - _.get(funcObject, 'iam.bindings') || [], + const iamBindings = (_.get(funcObject, 'iam.bindings') || []) + .concat( _.get(this, 'serverless.service.provider.iam.bindings') || [], allowUnauthenticated ? [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }] : [] - ), - (result, value) => { + ) + .reduce((result, value) => { result[value.role] = _.union(result[value.role] || [], value.members); return result; - }, - {} - ); + }, {}); if (!funcTemplate.properties.serviceAccountEmail) { delete funcTemplate.properties.serviceAccountEmail; From acba8ca678c929cd9d39226d2faebf490b811464 Mon Sep 17 00:00:00 2001 From: Erick Daniszewski Date: Wed, 12 May 2021 08:33:17 -0400 Subject: [PATCH 3/4] test updates --- package.json | 3 +- package/lib/compileFunctions.test.js | 148 +++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index c7a40aa..8a4ce89 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "chalk": "^3.0.0", "fs-extra": "^8.1.0", "googleapis": "^50.0.0", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "serverless": "^2.41.1" }, "devDependencies": { "@commitlint/cli": "^9.1.2", diff --git a/package/lib/compileFunctions.test.js b/package/lib/compileFunctions.test.js index c5c6044..b4e2cbc 100644 --- a/package/lib/compileFunctions.test.js +++ b/package/lib/compileFunctions.test.js @@ -121,6 +121,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -159,6 +164,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -197,6 +207,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -235,6 +250,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -277,6 +297,11 @@ describe('CompileFunctions', () => { test: 'label', }, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -319,6 +344,11 @@ describe('CompileFunctions', () => { test: 'label', }, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -366,6 +396,11 @@ describe('CompileFunctions', () => { secondTest: 'tested', }, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -409,6 +444,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -452,6 +492,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -502,6 +547,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -543,6 +593,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -601,6 +656,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, { type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', @@ -619,6 +679,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -660,6 +725,11 @@ describe('CompileFunctions', () => { labels: {}, vpcConnector: 'projects/pg-us-n-app-123456/locations/us-central1/connectors/my-vpc', }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -703,6 +773,11 @@ describe('CompileFunctions', () => { labels: {}, vpcConnector: 'projects/pg-us-n-app-123456/locations/us-central1/connectors/my-vpc', }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -752,6 +827,11 @@ describe('CompileFunctions', () => { labels: {}, vpcConnector: 'projects/pg-us-n-app-123456/locations/us-central1/connectors/my-vpc', }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, { type: 'gcp-types/cloudfunctions-v1:projects.locations.functions', @@ -771,6 +851,11 @@ describe('CompileFunctions', () => { labels: {}, vpcConnector: 'projects/pg-us-n-app-123456/locations/us-central1/connectors/my-vpc', }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -853,7 +938,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -864,6 +949,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; const functionIamBindings = { @@ -901,7 +991,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -912,6 +1002,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; const functionIamBindings = { @@ -956,7 +1051,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -968,6 +1063,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -1002,7 +1102,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -1014,6 +1114,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; @@ -1048,7 +1153,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -1059,6 +1164,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; const functionIamBindings = { @@ -1103,7 +1213,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -1114,6 +1224,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; const functionIamBindings = { @@ -1174,7 +1289,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -1185,6 +1300,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; const functionIamBindings = { @@ -1238,7 +1358,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -1249,6 +1369,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; const functionIamBindings = { @@ -1302,7 +1427,7 @@ describe('CompileFunctions', () => { name: 'my-service-dev-func1', properties: { parent: 'projects/myProject/locations/us-central1', - runtime: 'nodejs8', + runtime: 'nodejs10', function: 'my-service-dev-func1', entryPoint: 'func1', availableMemoryMb: 256, @@ -1313,6 +1438,11 @@ describe('CompileFunctions', () => { }, labels: {}, }, + accessControl: { + gcpIamPolicy: { + bindings: [], + }, + }, }, ]; const functionIamBindings = { From f3545366200904085aed3ea59bf3413736ed2665 Mon Sep 17 00:00:00 2001 From: Erick Daniszewski Date: Wed, 12 May 2021 08:43:58 -0400 Subject: [PATCH 4/4] replace BbPromise with native Promise --- deploy/lib/setIamPolicy.js | 9 +++++---- deploy/lib/setIamPolicy.test.js | 11 +++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/deploy/lib/setIamPolicy.js b/deploy/lib/setIamPolicy.js index d2bf7cc..2443902 100644 --- a/deploy/lib/setIamPolicy.js +++ b/deploy/lib/setIamPolicy.js @@ -1,11 +1,12 @@ 'use strict'; -const BbPromise = require('bluebird'); const ServerlessError = require('serverless/lib/classes/Error').ServerlessError; module.exports = { setIamPolicy() { - return BbPromise.bind(this).then(this.getFunctions).then(this.setPolicies); + return Promise.resolve() + .then(() => this.getFunctions()) + .then(() => this.setPolicies()); }, getFunctions() { @@ -29,7 +30,7 @@ module.exports = { // If there are no IAM policies configured with any function, there is nothing to // do here. if (!policies || !Object.keys(policies).length) { - return BbPromise.resolve(); + return Promise.resolve(); } this.serverless.cli.log('Setting IAM policies...'); @@ -67,6 +68,6 @@ module.exports = { } }); - return BbPromise.all(promises); + return Promise.all(promises); }, }; diff --git a/deploy/lib/setIamPolicy.test.js b/deploy/lib/setIamPolicy.test.js index d8e6d96..cef4a87 100644 --- a/deploy/lib/setIamPolicy.test.js +++ b/deploy/lib/setIamPolicy.test.js @@ -1,7 +1,6 @@ 'use strict'; const sinon = require('sinon'); -const BbPromise = require('bluebird'); const GoogleProvider = require('../../provider/googleProvider'); const GoogleDeploy = require('../googleDeploy'); @@ -39,8 +38,8 @@ describe('SetIamPolicy', () => { let setPoliciesStub; beforeEach(() => { - getFunctionsStub = sinon.stub(googleDeploy, 'getFunctions').returns(BbPromise.resolve()); - setPoliciesStub = sinon.stub(googleDeploy, 'setPolicies').returns(BbPromise.resolve()); + getFunctionsStub = sinon.stub(googleDeploy, 'getFunctions').returns(Promise.resolve()); + setPoliciesStub = sinon.stub(googleDeploy, 'setPolicies').returns(Promise.resolve()); }); afterEach(() => { @@ -58,7 +57,7 @@ describe('SetIamPolicy', () => { describe('#getFunctions', () => { it('should return "undefined" if no functions are found', () => { - requestStub.returns(BbPromise.resolve([])); + requestStub.returns(Promise.resolve([])); return googleDeploy.getFunctions().then((foundFunctions) => { expect(foundFunctions).toEqual(undefined); @@ -81,7 +80,7 @@ describe('SetIamPolicy', () => { const response = { functions: [{ name: 'cloud-function-1' }, { name: 'cloud-function-2' }], }; - requestStub.returns(BbPromise.resolve(response)); + requestStub.returns(Promise.resolve(response)); return googleDeploy.getFunctions().then((foundFunctions) => { expect(foundFunctions).toEqual([ @@ -160,7 +159,7 @@ describe('SetIamPolicy', () => { googleDeploy.serverless.service.provider.functionIamBindings = { 'cloud-function-2': [{ role: 'roles/cloudfunctions.invoker', members: ['allUsers'] }], }; - requestStub.returns(BbPromise.resolve()); + requestStub.returns(Promise.resolve()); return googleDeploy.setPolicies(foundFunctions).then(() => { expect(consoleLogStub.calledOnce).toEqual(true);