Skip to content

Commit

Permalink
feat: Support managedExternally option for authorizers (#7327)
Browse files Browse the repository at this point in the history
There are use cases where an API creator does not have permissions to
add permissions to the custom authorizer lambda; one example is when
the custom authorizer lambda exists in a separate AWS account. In these
cases, we need to be able to omit the `AWS::Lambda::Permission` resource
from the stack.

This change adds the `managedExternally` attribute to the `authorizer`.
When `managedExternally` is `true`, the stack will not create the
`AWS::Lambda::Permission` resource.

**Important note:** The permission does still need to be created before
the stack is deployed, or creating the authorizer will fail.
  • Loading branch information
glb authored Feb 12, 2020
1 parent 3399c96 commit 7abb23e
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 0 deletions.
21 changes: 21 additions & 0 deletions docs/providers/aws/events/apigateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,11 +444,32 @@ functions:
method: post
authorizer:
arn: xxx:xxx:Lambda-Name
managedExternally: false
resultTtlInSeconds: 0
identitySource: method.request.header.Authorization
identityValidationExpression: someRegex
```

If permissions for the Authorizer function are managed externally (for example, if the Authorizer function exists
in a different AWS account), you can skip creating the permission for the function by setting `managedExternally: true`,
as shown in the following example:

```yml
functions:
create:
handler: posts.create
events:
- http:
path: posts/create
method: post
authorizer:
arn: xxx:xxx:Lambda-Name
managedExternally: true
```

**IMPORTANT NOTE**: The permission allowing the authorizer function to be called by API Gateway must exist
before deploying the stack, otherwise deployment will fail.

You can also use the Request Type Authorizer by setting the `type` property. In this case, your `identitySource` could contain multiple entries for your policy cache. The default `type` is 'token'.

```yml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ module.exports = {
}

if (cfResources[authorizerPermissionLogicalId]) return;

if (authorizer.managedExternally) return;

Object.assign(cfResources, {
[authorizerPermissionLogicalId]: {
Type: 'AWS::Lambda::Permission',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,69 @@ describe('#awsCompilePermissions()', () => {
).to.deep.equal({});
});
});

it('should not create permission resources when the authorizer is managed externally', () => {
const event = {
functionName: 'First',
http: {
authorizer: {
name: 'authorizer',
arn: { 'Fn::GetAtt': ['AuthorizerLambdaFunction', 'Arn'] },
managedExternally: true,
},
path: 'foo/bar',
method: 'post',
},
};

awsCompileApigEvents.validated.events = [event];
awsCompileApigEvents.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
awsCompileApigEvents.permissionMapping = [
{
lambdaLogicalId: 'FirstLambdaFunction',
resourceName: 'FooBar',
event,
},
];

// the important thing in this object is that it does *not* contain
// a permission allowing API Gateway to call the authorizer. If
// managedExternally was false (as it is in other tests), then the
// permission would be created.
const deepObj = {
FirstLambdaPermissionApiGateway: {
DependsOn: undefined,
Properties: {
Action: 'lambda:InvokeFunction',
FunctionName: {
'Fn::GetAtt': ['FirstLambdaFunction', 'Arn'],
},
Principal: 'apigateway.amazonaws.com',
SourceArn: {
'Fn::Join': [
'',
[
'arn:',
{ Ref: 'AWS::Partition' },
':execute-api:',
{ Ref: 'AWS::Region' },
':',
{ Ref: 'AWS::AccountId' },
':',
{ Ref: 'ApiGatewayRestApi' },
'/*/*',
],
],
},
},
Type: 'AWS::Lambda::Permission',
},
};

return awsCompileApigEvents.compilePermissions().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
).to.deep.equal(deepObj);
});
});
});
18 changes: 18 additions & 0 deletions lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ module.exports = {
let type;
let name;
let arn;
let managedExternally;
let identitySource;
let resultTtlInSeconds;
let identityValidationExpression;
Expand Down Expand Up @@ -273,6 +274,18 @@ module.exports = {

identitySource = authorizer.identitySource;
identityValidationExpression = authorizer.identityValidationExpression;

if (typeof authorizer.managedExternally === 'undefined') {
managedExternally = false;
} else if (typeof authorizer.managedExternally === 'boolean') {
managedExternally = authorizer.managedExternally;
} else {
const errorMessage = [
`managedExternally property in authorizer for function ${functionName} is not boolean.`,
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
} else {
const errorMessage = [
`authorizer property in function ${functionName} is not an object nor a string.`,
Expand All @@ -283,6 +296,10 @@ module.exports = {
throw new this.serverless.classes.Error(errorMessage);
}

if (typeof managedExternally === 'undefined') {
managedExternally = false;
}

if (typeof identitySource === 'undefined') {
identitySource = 'method.request.header.Authorization';
}
Expand All @@ -305,6 +322,7 @@ module.exports = {
type,
name,
arn,
managedExternally,
authorizerId,
resultTtlInSeconds,
identitySource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ describe('#validate()', () => {
const authorizer = validated.events[0].http.authorizer;
expect(authorizer.resultTtlInSeconds).to.equal(300);
expect(authorizer.identitySource).to.equal('method.request.header.Authorization');
expect(authorizer.managedExternally).to.equal(false);
});

it('should accept authorizer config', () => {
Expand All @@ -465,6 +466,7 @@ describe('#validate()', () => {
resultTtlInSeconds: 500,
identitySource: 'method.request.header.Custom',
identityValidationExpression: 'foo',
managedExternally: true,
},
},
},
Expand All @@ -477,6 +479,7 @@ describe('#validate()', () => {
expect(authorizer.resultTtlInSeconds).to.equal(500);
expect(authorizer.identitySource).to.equal('method.request.header.Custom');
expect(authorizer.identityValidationExpression).to.equal('foo');
expect(authorizer.managedExternally).to.equal(true);
});

it('should accept authorizer config with a type', () => {
Expand Down Expand Up @@ -534,6 +537,31 @@ describe('#validate()', () => {
expect(authorizer.identityValidationExpression).to.equal('foo');
});

it('should throw an error if authorizer "managedExternally" exists and is not a boolean', () => {
awsCompileApigEvents.serverless.service.functions = {
foo: {},
first: {
events: [
{
http: {
method: 'GET',
path: 'foo/bar',
authorizer: {
name: 'foo',
resultTtlInSeconds: 0,
identitySource: 'method.request.header.Custom',
identityValidationExpression: 'foo',
managedExternally: 'not a boolean',
},
},
},
],
},
};

expect(() => awsCompileApigEvents.validate()).to.throw(Error, 'managedExternally property');
});

it('should throw an error if "origin" and "origins" CORS config is used', () => {
awsCompileApigEvents.serverless.service.functions = {
first: {
Expand Down

0 comments on commit 7abb23e

Please sign in to comment.