From a82e7bf2c87db9b0ab0b9a359c004b10908963c4 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Fri, 16 Aug 2019 13:27:01 +0300 Subject: [PATCH] fix: limit sqs,kinesis permissions --- .../service/serverless.yml | 45 ++++++++ .../kinesis/multiple-integrations/tests.js | 40 +++++++ .../service/serverless.yml | 2 +- .../kinesis/{ => single-integration}/tests.js | 16 +-- .../service/serverless.yml | 39 +++++++ .../sqs/multiple-integrations/tests.js | 44 ++++++++ .../service/serverless.yml | 6 +- .../sqs/{ => single-integration}/tests.js | 17 ++- __tests__/utils.js | 58 +++++++--- .../kinesis/compileIamRoleToKinesis.js | 97 ++++++++-------- .../kinesis/compileIamRoleToKinesis.test.js | 38 ++++++- lib/package/sqs/compileIamRoleToSqs.js | 105 +++++++++--------- lib/package/sqs/compileIamRoleToSqs.test.js | 34 +++++- package.json | 1 + 14 files changed, 399 insertions(+), 143 deletions(-) create mode 100644 __tests__/integration/kinesis/multiple-integrations/service/serverless.yml create mode 100644 __tests__/integration/kinesis/multiple-integrations/tests.js rename __tests__/integration/kinesis/{ => single-integration}/service/serverless.yml (91%) rename __tests__/integration/kinesis/{ => single-integration}/tests.js (66%) create mode 100644 __tests__/integration/sqs/multiple-integrations/service/serverless.yml create mode 100644 __tests__/integration/sqs/multiple-integrations/tests.js rename __tests__/integration/sqs/{ => single-integration}/service/serverless.yml (67%) rename __tests__/integration/sqs/{ => single-integration}/tests.js (73%) diff --git a/__tests__/integration/kinesis/multiple-integrations/service/serverless.yml b/__tests__/integration/kinesis/multiple-integrations/service/serverless.yml new file mode 100644 index 0000000..8cc1290 --- /dev/null +++ b/__tests__/integration/kinesis/multiple-integrations/service/serverless.yml @@ -0,0 +1,45 @@ +service: multiple-kinesis-proxy + +provider: + name: aws + runtime: nodejs10.x + +plugins: + localPath: './../../../../../../' + modules: + - serverless-apigateway-service-proxy + +custom: + apiGatewayServiceProxies: + - kinesis: + path: /kinesis1 + method: post + streamName: { Ref: 'YourStream1' } + cors: true + + - kinesis: + path: /kinesis2 + method: post + streamName: { Ref: 'YourStream2' } + cors: true + + - kinesis: + path: /kinesis3 + method: post + streamName: { Ref: 'YourStream3' } + cors: true + +resources: + Resources: + YourStream1: + Type: AWS::Kinesis::Stream + Properties: + ShardCount: 1 + YourStream2: + Type: AWS::Kinesis::Stream + Properties: + ShardCount: 1 + YourStream3: + Type: AWS::Kinesis::Stream + Properties: + ShardCount: 1 diff --git a/__tests__/integration/kinesis/multiple-integrations/tests.js b/__tests__/integration/kinesis/multiple-integrations/tests.js new file mode 100644 index 0000000..47a2cc2 --- /dev/null +++ b/__tests__/integration/kinesis/multiple-integrations/tests.js @@ -0,0 +1,40 @@ +'use strict' + +const expect = require('chai').expect +const fetch = require('node-fetch') +const { deployWithRandomStage, removeService } = require('../../../utils') + +describe('Multiple Kinesis Proxy Integrations Test', () => { + let endpoint + let stage + const config = '__tests__/integration/kinesis/multiple-integrations/service/serverless.yml' + + beforeAll(async () => { + const result = await deployWithRandomStage(config) + stage = result.stage + endpoint = result.endpoint + }) + + afterAll(() => { + removeService(stage, config) + }) + + it('should get correct response from multiple kinesis proxy endpoints', async () => { + const streams = ['kinesis1', 'kinesis2', 'kinesis3'] + + for (const stream of streams) { + const testEndpoint = `${endpoint}/${stream}` + + const response = await fetch(testEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ Data: `data for stream ${stream}`, PartitionKey: 'some key' }) + }) + expect(response.headers.get('access-control-allow-origin')).to.deep.equal('*') + expect(response.status).to.be.equal(200) + const body = await response.json() + expect(body).to.have.own.property('ShardId') + expect(body).to.have.own.property('SequenceNumber') + } + }) +}) diff --git a/__tests__/integration/kinesis/service/serverless.yml b/__tests__/integration/kinesis/single-integration/service/serverless.yml similarity index 91% rename from __tests__/integration/kinesis/service/serverless.yml rename to __tests__/integration/kinesis/single-integration/service/serverless.yml index 25f01ca..cdefb59 100644 --- a/__tests__/integration/kinesis/service/serverless.yml +++ b/__tests__/integration/kinesis/single-integration/service/serverless.yml @@ -5,7 +5,7 @@ provider: runtime: nodejs10.x plugins: - localPath: './../' + localPath: './../../../../../../' modules: - serverless-apigateway-service-proxy diff --git a/__tests__/integration/kinesis/tests.js b/__tests__/integration/kinesis/single-integration/tests.js similarity index 66% rename from __tests__/integration/kinesis/tests.js rename to __tests__/integration/kinesis/single-integration/tests.js index c253551..4d932e2 100644 --- a/__tests__/integration/kinesis/tests.js +++ b/__tests__/integration/kinesis/single-integration/tests.js @@ -2,21 +2,17 @@ const expect = require('chai').expect const fetch = require('node-fetch') -const { deployService, removeService, getApiGatewayEndpoint } = require('./../../utils') +const { deployWithRandomStage, removeService } = require('../../../utils') -describe('Kinesis Proxy Integration Test', () => { +describe('Single Kinesis Proxy Integration Test', () => { let endpoint - let stackName let stage - const config = '__tests__/integration/kinesis/service/serverless.yml' + const config = '__tests__/integration/kinesis/single-integration/service/serverless.yml' beforeAll(async () => { - stage = Math.random() - .toString(32) - .substring(2) - stackName = 'kinesis-proxy-' + stage - deployService(stage, config) - endpoint = await getApiGatewayEndpoint(stackName) + const result = await deployWithRandomStage(config) + stage = result.stage + endpoint = result.endpoint }) afterAll(() => { diff --git a/__tests__/integration/sqs/multiple-integrations/service/serverless.yml b/__tests__/integration/sqs/multiple-integrations/service/serverless.yml new file mode 100644 index 0000000..df9565a --- /dev/null +++ b/__tests__/integration/sqs/multiple-integrations/service/serverless.yml @@ -0,0 +1,39 @@ +service: multiple-sqs-proxy + +provider: + name: aws + runtime: nodejs10.x + +plugins: + localPath: './../../../../../../' + modules: + - serverless-apigateway-service-proxy + +custom: + apiGatewayServiceProxies: + - sqs: + path: /sqs1 + method: post + queueName: { 'Fn::GetAtt': ['SQSQueue1', 'QueueName'] } + cors: true + + - sqs: + path: /sqs2 + method: post + queueName: { 'Fn::GetAtt': ['SQSQueue2', 'QueueName'] } + cors: true + + - sqs: + path: /sqs3 + method: post + queueName: { 'Fn::GetAtt': ['SQSQueue3', 'QueueName'] } + cors: true + +resources: + Resources: + SQSQueue1: + Type: 'AWS::SQS::Queue' + SQSQueue2: + Type: 'AWS::SQS::Queue' + SQSQueue3: + Type: 'AWS::SQS::Queue' diff --git a/__tests__/integration/sqs/multiple-integrations/tests.js b/__tests__/integration/sqs/multiple-integrations/tests.js new file mode 100644 index 0000000..c3f4097 --- /dev/null +++ b/__tests__/integration/sqs/multiple-integrations/tests.js @@ -0,0 +1,44 @@ +'use strict' + +const expect = require('chai').expect +const fetch = require('node-fetch') +const { deployWithRandomStage, removeService } = require('../../../utils') + +describe('Multiple SQS Proxy Integrations Test', () => { + let endpoint + let stage + const config = '__tests__/integration/sqs/multiple-integrations/service/serverless.yml' + + beforeAll(async () => { + const result = await deployWithRandomStage(config) + stage = result.stage + endpoint = result.endpoint + }) + + afterAll(() => { + removeService(stage, config) + }) + + it('should get correct response from multiple sqs proxy endpoints', async () => { + const queues = ['sqs1', 'sqs2', 'sqs3'] + + for (const queue of queues) { + const testEndpoint = `${endpoint}/${queue}` + const response = await fetch(testEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: `message for ${queue}` }) + }) + expect(response.headers.get('access-control-allow-origin')).to.deep.equal('*') + expect(response.status).to.be.equal(200) + const body = await response.json() + expect(body.SendMessageResponse.SendMessageResult).to.have.own.property( + 'MD5OfMessageAttributes' + ) + expect(body.SendMessageResponse.SendMessageResult).to.have.own.property('MD5OfMessageBody') + expect(body.SendMessageResponse.SendMessageResult).to.have.own.property('MessageId') + expect(body.SendMessageResponse.SendMessageResult).to.have.own.property('SequenceNumber') + expect(body.SendMessageResponse.ResponseMetadata).to.have.own.property('RequestId') + } + }) +}) diff --git a/__tests__/integration/sqs/service/serverless.yml b/__tests__/integration/sqs/single-integration/service/serverless.yml similarity index 67% rename from __tests__/integration/sqs/service/serverless.yml rename to __tests__/integration/sqs/single-integration/service/serverless.yml index 4179166..3781baa 100644 --- a/__tests__/integration/sqs/service/serverless.yml +++ b/__tests__/integration/sqs/single-integration/service/serverless.yml @@ -5,7 +5,7 @@ provider: runtime: nodejs10.x plugins: - localPath: './../' + localPath: './../../../../../../' modules: - serverless-apigateway-service-proxy @@ -14,10 +14,10 @@ custom: - sqs: path: /sqs method: post - queueName: {"Fn::GetAtt":[ "SQSQueue", "QueueName" ]} + queueName: { 'Fn::GetAtt': ['SQSQueue', 'QueueName'] } cors: true resources: Resources: SQSQueue: - Type: "AWS::SQS::Queue" + Type: 'AWS::SQS::Queue' diff --git a/__tests__/integration/sqs/tests.js b/__tests__/integration/sqs/single-integration/tests.js similarity index 73% rename from __tests__/integration/sqs/tests.js rename to __tests__/integration/sqs/single-integration/tests.js index cef0b3a..cff3d4d 100644 --- a/__tests__/integration/sqs/tests.js +++ b/__tests__/integration/sqs/single-integration/tests.js @@ -2,21 +2,18 @@ const expect = require('chai').expect const fetch = require('node-fetch') -const { deployService, removeService, getApiGatewayEndpoint } = require('./../../utils') +const { deployWithRandomStage, removeService } = require('../../../utils') -describe('SQS Proxy Integration Test', () => { +describe('Single SQS Proxy Integration Test', () => { let endpoint - let stackName let stage - const config = '__tests__/integration/sqs/service/serverless.yml' + const config = '__tests__/integration/sqs/single-integration/service/serverless.yml' beforeAll(async () => { - stage = Math.random() - .toString(32) - .substring(2) - stackName = 'sqs-proxy-' + stage - deployService(stage, config) - endpoint = await getApiGatewayEndpoint(stackName) + const result = await deployWithRandomStage(config) + + stage = result.stage + endpoint = result.endpoint }) afterAll(() => { diff --git a/__tests__/utils.js b/__tests__/utils.js index fcbbeda..5e18559 100644 --- a/__tests__/utils.js +++ b/__tests__/utils.js @@ -1,26 +1,50 @@ 'use strict' const _ = require('lodash') +const yaml = require('js-yaml') +const fs = require('fs') +const path = require('path') const execSync = require('child_process').execSync const aws = require('aws-sdk') const cloudformation = new aws.CloudFormation({ region: 'us-east-1' }) +async function getApiGatewayEndpoint(stackName) { + const result = await cloudformation.describeStacks({ StackName: stackName }).promise() + + const endpointOutput = _.find(result.Stacks[0].Outputs, { OutputKey: 'ServiceEndpoint' }) + .OutputValue + return endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0] +} + +function deployService(stage, config) { + execSync(`npx serverless deploy --stage ${stage} --config ${path.basename(config)}`, { + stdio: 'inherit', + cwd: path.dirname(config) + }) +} + +function removeService(stage, config) { + execSync(`npx serverless remove --stage ${stage} --config ${path.basename(config)}`, { + stdio: 'inherit', + cwd: path.dirname(config) + }) +} + +async function deployWithRandomStage(config) { + const serviceName = yaml.safeLoad(fs.readFileSync(config)).service + const stage = Math.random() + .toString(32) + .substring(2) + const stackName = `${serviceName}-${stage}` + deployService(stage, config) + const endpoint = await getApiGatewayEndpoint(stackName) + + return { stage, endpoint } +} + module.exports = { - async getApiGatewayEndpoint(stackName) { - const result = await cloudformation.describeStacks({ StackName: stackName }).promise() - - const endpointOutput = _.find(result.Stacks[0].Outputs, { OutputKey: 'ServiceEndpoint' }) - .OutputValue - return endpointOutput.match(/https:\/\/.+\.execute-api\..+\.amazonaws\.com.+/)[0] - }, - - deployService(stage, config) { - execSync(`npx serverless deploy --stage ${stage} --config ${config}`, { - stdio: 'inherit' - }) - }, - - removeService(stage, config) { - execSync(`npx serverless remove --stage ${stage} --config ${config}`, { stdio: 'inherit' }) - } + getApiGatewayEndpoint, + deployService, + removeService, + deployWithRandomStage } diff --git a/lib/package/kinesis/compileIamRoleToKinesis.js b/lib/package/kinesis/compileIamRoleToKinesis.js index 5d58104..4d6cd74 100644 --- a/lib/package/kinesis/compileIamRoleToKinesis.js +++ b/lib/package/kinesis/compileIamRoleToKinesis.js @@ -1,60 +1,67 @@ 'use strict' const _ = require('lodash') -const BbPromise = require('bluebird') module.exports = { async compileIamRoleToKinesis() { - await BbPromise.all( - this.getAllServiceProxies().map(async (serviceProxy) => { + const kinesisStreamNames = this.getAllServiceProxies() + .filter((serviceProxy) => this.getServiceName(serviceProxy) === 'kinesis') + .map((serviceProxy) => { const serviceName = this.getServiceName(serviceProxy) - if (serviceName == 'kinesis') { - const template = { - Type: 'AWS::IAM::Role', - Properties: { - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'apigateway.amazonaws.com' - }, - Action: 'sts:AssumeRole' - } - ] + const { streamName } = serviceProxy[serviceName] + return streamName + }) + + if (kinesisStreamNames.length <= 0) { + return + } + + const policyResource = kinesisStreamNames.map((streamName) => ({ + 'Fn::Sub': [ + 'arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/${streamName}', + { streamName } + ] + })) + + const template = { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'apigateway.amazonaws.com' }, - Policies: [ + Action: 'sts:AssumeRole' + } + ] + }, + Policies: [ + { + PolicyName: 'apigatewaytokinesis', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + Resource: '*' + }, { - PolicyName: 'apigatewaytokinesis', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'logs:CreateLogGroup', - 'logs:CreateLogStream', - 'logs:PutLogEvents' - ], - Resource: '*' - }, - { - Effect: 'Allow', - Action: ['kinesis:PutRecord'], - Resource: '*' - } - ] - } + Effect: 'Allow', + Action: ['kinesis:PutRecord'], + Resource: policyResource } ] } } + ] + } + } - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { - ApigatewayToKinesisRole: template - }) - } - }) - ) + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + ApigatewayToKinesisRole: template + }) } } diff --git a/lib/package/kinesis/compileIamRoleToKinesis.test.js b/lib/package/kinesis/compileIamRoleToKinesis.test.js index 4105e89..e476cbd 100644 --- a/lib/package/kinesis/compileIamRoleToKinesis.test.js +++ b/lib/package/kinesis/compileIamRoleToKinesis.test.js @@ -30,8 +30,23 @@ describe('#compileIamRoleToKinesis()', () => { apiGatewayServiceProxies: [ { kinesis: { - path: '/kinesis', - method: 'post' + path: '/kinesis1', + method: 'post', + streamName: { Ref: 'KinesisStream1' } + } + }, + { + kinesis: { + path: '/kinesis2', + method: 'post', + streamName: { Ref: 'KinesisStream2' } + } + }, + { + sqs: { + path: '/sqs', + method: 'post', + queueName: 'MyQueue' } } ] @@ -63,7 +78,24 @@ describe('#compileIamRoleToKinesis()', () => { Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], Resource: '*' }, - { Effect: 'Allow', Action: ['kinesis:PutRecord'], Resource: '*' } + { + Effect: 'Allow', + Action: ['kinesis:PutRecord'], + Resource: [ + { + 'Fn::Sub': [ + 'arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/${streamName}', + { streamName: { Ref: 'KinesisStream1' } } + ] + }, + { + 'Fn::Sub': [ + 'arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:stream/${streamName}', + { streamName: { Ref: 'KinesisStream2' } } + ] + } + ] + } ] } } diff --git a/lib/package/sqs/compileIamRoleToSqs.js b/lib/package/sqs/compileIamRoleToSqs.js index bc45061..f764bab 100644 --- a/lib/package/sqs/compileIamRoleToSqs.js +++ b/lib/package/sqs/compileIamRoleToSqs.js @@ -1,61 +1,64 @@ 'use strict' const _ = require('lodash') -const BbPromise = require('bluebird') module.exports = { async compileIamRoleToSqs() { - await BbPromise.all( - this.getAllServiceProxies().map(async (serviceProxy) => { - Object.keys(serviceProxy).forEach(async (serviceName) => { - if (serviceName == 'sqs') { - const template = { - Type: 'AWS::IAM::Role', - Properties: { - AssumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'apigateway.amazonaws.com' - }, - Action: 'sts:AssumeRole' - } - ] + const sqsQueueNames = this.getAllServiceProxies() + .filter((serviceProxy) => this.getServiceName(serviceProxy) === 'sqs') + .map((serviceProxy) => { + const serviceName = this.getServiceName(serviceProxy) + const { queueName } = serviceProxy[serviceName] + return queueName + }) + + if (sqsQueueNames.length <= 0) { + return + } + + const policyResource = sqsQueueNames.map((queueName) => ({ + 'Fn::Sub': ['arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${queueName}', { queueName }] + })) + + const template = { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'apigateway.amazonaws.com' + }, + Action: 'sts:AssumeRole' + } + ] + }, + Policies: [ + { + PolicyName: 'apigatewaytosqs', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + Resource: '*' }, - Policies: [ - { - PolicyName: 'apigatewaytosqs', - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Action: [ - 'logs:CreateLogGroup', - 'logs:CreateLogStream', - 'logs:PutLogEvents' - ], - Resource: '*' - }, - { - Effect: 'Allow', - Action: ['sqs:SendMessage'], - Resource: '*' - } - ] - } - } - ] - } + { + Effect: 'Allow', + Action: ['sqs:SendMessage'], + Resource: policyResource + } + ] } - - _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { - ApigatewayToSqsRole: template - }) } - }) - }) - ) + ] + } + } + + _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, { + ApigatewayToSqsRole: template + }) } } diff --git a/lib/package/sqs/compileIamRoleToSqs.test.js b/lib/package/sqs/compileIamRoleToSqs.test.js index 4375b8c..04ac14a 100644 --- a/lib/package/sqs/compileIamRoleToSqs.test.js +++ b/lib/package/sqs/compileIamRoleToSqs.test.js @@ -30,8 +30,23 @@ describe('#compileIamRoleToSqs()', () => { apiGatewayServiceProxies: [ { sqs: { - path: '/sqs', - method: 'post' + path: '/sqs1', + method: 'post', + queueName: { 'Fn::GetAtt': ['SQSQueue1', 'QueueName'] } + } + }, + { + sqs: { + path: '/sqs2', + method: 'post', + queueName: { 'Fn::GetAtt': ['SQSQueue2', 'QueueName'] } + } + }, + { + kinesis: { + path: '/kinesis', + method: 'post', + streamName: { Ref: 'KinesisStream' } } } ] @@ -68,7 +83,20 @@ describe('#compileIamRoleToSqs()', () => { { Effect: 'Allow', Action: ['sqs:SendMessage'], - Resource: '*' + Resource: [ + { + 'Fn::Sub': [ + 'arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${queueName}', + { queueName: { 'Fn::GetAtt': ['SQSQueue1', 'QueueName'] } } + ] + }, + { + 'Fn::Sub': [ + 'arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${queueName}', + { queueName: { 'Fn::GetAtt': ['SQSQueue2', 'QueueName'] } } + ] + } + ] } ] } diff --git a/package.json b/package.json index c40f540..27c28c4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "istanbul": "^1.0.0-alpha.2", "jest-circus": "^24.8.0", "jest-cli": "^24.8.0", + "js-yaml": "^3.13.1", "lint-staged": "^9.2.0", "mocha": "^6.2.0", "mocha-lcov-reporter": "^1.3.0",