Skip to content

Commit 6158854

Browse files
authored
Merge pull request #60 from AhmedNourJamalElDin/master
Allow Private Field
2 parents 4f6bb66 + bc87601 commit 6158854

8 files changed

+667
-0
lines changed

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This Serverless Framework plugin supports the AWS service proxy integration feat
2222
- [Common API Gateway features](#common-api-gateway-features)
2323
- [Enabling CORS](#enabling-cors)
2424
- [Adding Authorization](#adding-authorization)
25+
- [Enabling API Token Authentication](#enabling-api-token-authentication)
2526
- [Using a Custom IAM Role](#using-a-custom-iam-role)
2627
- [Customizing API Gateway parameters](#customizing-api-gateway-parameters)
2728
- [Customizing request body mapping templates](#customizing-request-body-mapping-templates)
@@ -480,6 +481,35 @@ resources:
480481

481482
Source: [AWS::ApiGateway::Method docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-authorizationtype)
482483

484+
485+
486+
### Enabling API Token Authentication
487+
488+
You can indicate whether the method requires clients to submit a valid API key using `private` flag:
489+
490+
```yml
491+
custom:
492+
apiGatewayServiceProxies:
493+
- sqs:
494+
path: /sqs
495+
method: post
496+
queueName: { 'Fn::GetAtt': ['SQSQueue', 'QueueName'] }
497+
cors: true
498+
private: true
499+
500+
resources:
501+
Resources:
502+
SQSQueue:
503+
Type: 'AWS::SQS::Queue'
504+
```
505+
506+
which is the same syntax used in Serverless framework.
507+
508+
Source: [Serverless: Setting API keys for your Rest API](https://serverless.com/framework/docs/providers/aws/events/apigateway/#setting-api-keys-for-your-rest-api)
509+
510+
Source: [AWS::ApiGateway::Method docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-method.html#cfn-apigateway-method-apikeyrequired)
511+
512+
483513
### Using a Custom IAM Role
484514

485515
By default, the plugin will generate a role with the required permissions for each service type that is configured.

lib/apiGateway/schema.js

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const method = Joi.string()
2323
.valid(['get', 'post', 'put', 'patch', 'options', 'head', 'delete', 'any'])
2424
.insensitive()
2525

26+
const privateField = Joi.boolean().default(false)
27+
2628
const cors = Joi.alternatives().try(
2729
Joi.boolean(),
2830
Joi.object({
@@ -98,6 +100,7 @@ const proxy = Joi.object({
98100
pathOverride,
99101
method,
100102
cors,
103+
private: privateField,
101104
authorizationType,
102105
authorizerId,
103106
authorizationScopes,

lib/apiGateway/validate.test.js

+69
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,75 @@ describe('#validateServiceProxies()', () => {
137137
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
138138
})
139139

140+
it('should work if "private" property is missing', () => {
141+
serverlessApigatewayServiceProxy.serverless.service.custom = {
142+
apiGatewayServiceProxies: [
143+
{
144+
kinesis: {
145+
path: '/kinesis',
146+
streamName: 'streamName',
147+
method: 'POST'
148+
}
149+
}
150+
]
151+
}
152+
153+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
154+
})
155+
156+
it('should work if "private" property is true', () => {
157+
serverlessApigatewayServiceProxy.serverless.service.custom = {
158+
apiGatewayServiceProxies: [
159+
{
160+
kinesis: {
161+
path: '/kinesis',
162+
streamName: 'streamName',
163+
method: 'POST',
164+
private: true
165+
}
166+
}
167+
]
168+
}
169+
170+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
171+
})
172+
173+
it('should work if "private" property is false', () => {
174+
serverlessApigatewayServiceProxy.serverless.service.custom = {
175+
apiGatewayServiceProxies: [
176+
{
177+
kinesis: {
178+
path: '/kinesis',
179+
streamName: 'streamName',
180+
method: 'POST',
181+
private: false
182+
}
183+
}
184+
]
185+
}
186+
187+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
188+
})
189+
190+
it('should throw if "private" property is set to unsupported type', () => {
191+
serverlessApigatewayServiceProxy.serverless.service.custom = {
192+
apiGatewayServiceProxies: [
193+
{
194+
kinesis: {
195+
path: '/kinesis',
196+
streamName: 'streamName',
197+
method: 'POST',
198+
private: 'xxxxx'
199+
}
200+
}
201+
]
202+
}
203+
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
204+
serverless.classes.Error,
205+
'child "kinesis" fails because [child "private" fails because ["private" must be a boolean]]'
206+
)
207+
})
208+
140209
it('should process cors defaults', () => {
141210
serverlessApigatewayServiceProxy.serverless.service.custom = {
142211
apiGatewayServiceProxies: [

lib/package/dynamodb/compileMethodsToDynamodb.test.js

+111
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,117 @@ describe('#compileMethodsToDynamodb()', () => {
825825
})
826826
})
827827
})
828+
829+
describe('#private', () => {
830+
it('should create corresponding resources when a dynamodb proxy is given with private', () => {
831+
serverlessApigatewayServiceProxy.validated = {
832+
events: [
833+
{
834+
serviceName: 'dynamodb',
835+
http: {
836+
path: 'dynamodb',
837+
method: 'post',
838+
action: 'PutItem',
839+
tableName: {
840+
Ref: 'MyTable'
841+
},
842+
key: 'key',
843+
private: true,
844+
auth: { authorizationType: 'NONE' }
845+
}
846+
}
847+
]
848+
}
849+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
850+
serverlessApigatewayServiceProxy.apiGatewayResources = {
851+
dynamodb: {
852+
name: 'dynamodb',
853+
resourceLogicalId: 'ApiGatewayResourceDynamodb'
854+
}
855+
}
856+
857+
serverlessApigatewayServiceProxy.compileMethodsToDynamodb()
858+
expect(serverless.service.provider.compiledCloudFormationTemplate.Resources).to.deep.equal({
859+
ApiGatewayMethoddynamodbPost: {
860+
Type: 'AWS::ApiGateway::Method',
861+
Properties: {
862+
HttpMethod: 'POST',
863+
RequestParameters: {},
864+
AuthorizationScopes: undefined,
865+
AuthorizerId: undefined,
866+
AuthorizationType: 'NONE',
867+
ApiKeyRequired: true,
868+
ResourceId: { Ref: 'ApiGatewayResourceDynamodb' },
869+
RestApiId: { Ref: 'ApiGatewayRestApi' },
870+
Integration: {
871+
IntegrationHttpMethod: 'POST',
872+
Type: 'AWS',
873+
Credentials: { 'Fn::GetAtt': ['ApigatewayToDynamodbRole', 'Arn'] },
874+
Uri: {
875+
'Fn::Sub': [
876+
'arn:aws:apigateway:${AWS::Region}:dynamodb:action/${action}',
877+
{ action: 'PutItem' }
878+
]
879+
},
880+
PassthroughBehavior: 'NEVER',
881+
RequestTemplates: {
882+
'application/json': {
883+
'Fn::Sub': [
884+
'{"TableName": "${TableName}","Item": {\n #set ($body = $util.parseJson($input.body))\n #foreach( $key in $body.keySet())\n #set ($item = $body.get($key))\n #foreach( $type in $item.keySet())\n "$key":{"$type" : "$item.get($type)"}\n #if($foreach.hasNext()),#end\n #end\n #if($foreach.hasNext()),#end\n #end\n }\n }',
885+
{ TableName: { Ref: 'MyTable' } }
886+
]
887+
},
888+
'application/x-www-form-urlencoded': {
889+
'Fn::Sub': [
890+
'{"TableName": "${TableName}","Item": {\n #set ($body = $util.parseJson($input.body))\n #foreach( $key in $body.keySet())\n #set ($item = $body.get($key))\n #foreach( $type in $item.keySet())\n "$key":{"$type" : "$item.get($type)"}\n #if($foreach.hasNext()),#end\n #end\n #if($foreach.hasNext()),#end\n #end\n }\n }',
891+
{ TableName: { Ref: 'MyTable' } }
892+
]
893+
}
894+
},
895+
IntegrationResponses: [
896+
{
897+
StatusCode: 200,
898+
SelectionPattern: '2\\d{2}',
899+
ResponseParameters: {},
900+
ResponseTemplates: {}
901+
},
902+
{
903+
StatusCode: 400,
904+
SelectionPattern: '4\\d{2}',
905+
ResponseParameters: {},
906+
ResponseTemplates: {}
907+
},
908+
{
909+
StatusCode: 500,
910+
SelectionPattern: '5\\d{2}',
911+
ResponseParameters: {},
912+
ResponseTemplates: {}
913+
}
914+
]
915+
},
916+
MethodResponses: [
917+
{
918+
ResponseParameters: {},
919+
ResponseModels: {},
920+
StatusCode: 200
921+
},
922+
{
923+
ResponseParameters: {},
924+
ResponseModels: {},
925+
StatusCode: 400
926+
},
927+
{
928+
ResponseParameters: {},
929+
ResponseModels: {},
930+
StatusCode: 500
931+
}
932+
]
933+
}
934+
}
935+
})
936+
})
937+
})
938+
828939
describe('#authorization', () => {
829940
const testAuthorization = (auth) => {
830941
const param = {

lib/package/kinesis/compileMethodsToKinesis.test.js

+112
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,118 @@ describe('#compileMethodsToKinesis()', () => {
249249
})
250250
})
251251

252+
it('should create corresponding resources when kinesis proxies are given with private', () => {
253+
serverlessApigatewayServiceProxy.validated = {
254+
events: [
255+
{
256+
serviceName: 'kinesis',
257+
http: {
258+
streamName: 'myStream',
259+
path: 'kinesis',
260+
method: 'post',
261+
auth: {
262+
authorizationType: 'NONE'
263+
},
264+
private: true
265+
}
266+
}
267+
]
268+
}
269+
serverlessApigatewayServiceProxy.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi'
270+
serverlessApigatewayServiceProxy.apiGatewayResources = {
271+
kinesis: {
272+
name: 'kinesis',
273+
resourceLogicalId: 'ApiGatewayResourceKinesis'
274+
}
275+
}
276+
277+
serverlessApigatewayServiceProxy.compileMethodsToKinesis()
278+
279+
expect(serverless.service.provider.compiledCloudFormationTemplate.Resources).to.deep.equal({
280+
ApiGatewayMethodkinesisPost: {
281+
Type: 'AWS::ApiGateway::Method',
282+
Properties: {
283+
HttpMethod: 'POST',
284+
RequestParameters: {},
285+
AuthorizationType: 'NONE',
286+
AuthorizerId: undefined,
287+
AuthorizationScopes: undefined,
288+
ApiKeyRequired: true,
289+
ResourceId: { Ref: 'ApiGatewayResourceKinesis' },
290+
RestApiId: { Ref: 'ApiGatewayRestApi' },
291+
Integration: {
292+
IntegrationHttpMethod: 'POST',
293+
Type: 'AWS',
294+
Credentials: { 'Fn::GetAtt': ['ApigatewayToKinesisRole', 'Arn'] },
295+
Uri: {
296+
'Fn::Sub': 'arn:aws:apigateway:${AWS::Region}:kinesis:action/PutRecord'
297+
},
298+
PassthroughBehavior: 'NEVER',
299+
RequestTemplates: {
300+
'application/json': {
301+
'Fn::Sub': [
302+
'{"StreamName":"${StreamName}","Data":"${Data}","PartitionKey":"${PartitionKey}"}',
303+
{
304+
StreamName: 'myStream',
305+
Data: '$util.base64Encode($input.body)',
306+
PartitionKey: '$context.requestId'
307+
}
308+
]
309+
},
310+
'application/x-www-form-urlencoded': {
311+
'Fn::Sub': [
312+
'{"StreamName":"${StreamName}","Data":"${Data}","PartitionKey":"${PartitionKey}"}',
313+
{
314+
StreamName: 'myStream',
315+
Data: '$util.base64Encode($input.body)',
316+
PartitionKey: '$context.requestId'
317+
}
318+
]
319+
}
320+
},
321+
IntegrationResponses: [
322+
{
323+
StatusCode: 200,
324+
SelectionPattern: 200,
325+
ResponseParameters: {},
326+
ResponseTemplates: {}
327+
},
328+
{
329+
StatusCode: 400,
330+
SelectionPattern: 400,
331+
ResponseParameters: {},
332+
ResponseTemplates: {}
333+
},
334+
{
335+
StatusCode: 500,
336+
SelectionPattern: 500,
337+
ResponseParameters: {},
338+
ResponseTemplates: {}
339+
}
340+
]
341+
},
342+
MethodResponses: [
343+
{
344+
ResponseParameters: {},
345+
ResponseModels: {},
346+
StatusCode: 200
347+
},
348+
{
349+
ResponseParameters: {},
350+
ResponseModels: {},
351+
StatusCode: 400
352+
},
353+
{
354+
ResponseParameters: {},
355+
ResponseModels: {},
356+
StatusCode: 500
357+
}
358+
]
359+
}
360+
}
361+
})
362+
})
363+
252364
it('should return the default template for application/json when one is not given', () => {
253365
const httpWithoutRequestTemplate = {
254366
path: 'foo/bar1',

0 commit comments

Comments
 (0)