Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(lambda): unlock use case for cross-account functions w/ preconfigured permissions #18979

Merged
merged 5 commits into from Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-lambda/README.md
Expand Up @@ -480,6 +480,35 @@ fn.addEventSource(new eventsources.S3EventSource(bucket, {

See the documentation for the __@aws-cdk/aws-lambda-event-sources__ module for more details.

## Imported Lambdas

When referencing an imported lambda in the CDK, use `fromFunctionArn()` for most use cases:

```ts
const fn = lambda.Function.fromFunctionArn(
this,
'Function',
'arn:aws:lambda:us-east-1:123456789012:function:MyFn',
);
```

The `fromFunctionAttributes()` API is available for more specific use cases:

```ts
const fn = lambda.Function.fromFunctionAttributes(this, 'Function', {
functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn',
// The following are optional properties for specific use cases and should be used with caution:

// Use Case: imported function is in the same account as the stack. This tells the CDK that it
// can modify the function's permissions.
sameEnvironment: true,

// Use Case: imported function is in a different account and user commits to ensuring that the
// imported function has the correct permissions outside the CDK.
skipPermissions: true,
});
```

## Lambda with DLQ

A dead-letter queue can be automatically created for a Lambda function by
Expand Down
30 changes: 27 additions & 3 deletions packages/@aws-cdk/aws-lambda/lib/function-base.ts
Expand Up @@ -180,6 +180,20 @@ export interface FunctionAttributes {
*/
readonly sameEnvironment?: boolean;

/**
* Setting this property informs the CDK that the imported function ALREADY HAS the necessary permissions
* for what you are trying to do. When not configured, the CDK attempts to auto-determine whether or not
* additional permissions are necessary on the function when grant APIs are used. If the CDK tried to add
* permissions on an imported lambda, it will fail.
*
* Set this property *ONLY IF* you are committing to manage the imported function's permissions outside of
* CDK. You are acknowledging that your CDK code alone will have insufficient permissions to access the
* imported function.
*
* @default false
*/
readonly skipPermissions?: boolean;

/**
* The architecture of this Lambda Function (this is an optional attribute and defaults to X86_64).
* @default - Architecture.X86_64
Expand Down Expand Up @@ -228,6 +242,15 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
*/
protected abstract readonly canCreatePermissions: boolean;

/**
* Whether the user decides to skip adding permissions.
* The only use case is for cross-account, imported lambdas
* where the user commits to modifying the permisssions
* on the imported lambda outside CDK.
* @internal
*/
protected readonly _skipPermissions?: boolean;

/**
* Actual connections object for this Lambda
*
Expand Down Expand Up @@ -342,9 +365,10 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
});

const permissionNode = this._functionNode().tryFindChild(identifier);
if (!permissionNode) {
throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version. '
+ 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.');
if (!permissionNode && !this._skipPermissions) {
throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n'
+ 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.\n'
+ 'If the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.');
}
return { statementAdded: true, policyDependable: permissionNode };
},
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-lambda/lib/function.ts
Expand Up @@ -453,6 +453,7 @@ export class Function extends FunctionBase {
public readonly architecture = attrs.architecture ?? Architecture.X86_64;

protected readonly canCreatePermissions = attrs.sameEnvironment ?? this._isStackAccount();
protected readonly _skipPermissions = attrs.skipPermissions ?? false;

constructor(s: Construct, i: string) {
super(s, i, {
Expand Down
22 changes: 19 additions & 3 deletions packages/@aws-cdk/aws-lambda/test/function.test.ts
Expand Up @@ -845,7 +845,6 @@ describe('function', () => {
});

describe('grantInvoke', () => {

test('adds iam:InvokeFunction', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down Expand Up @@ -1091,8 +1090,25 @@ describe('function', () => {
const fn = lambda.Function.fromFunctionArn(stack, 'Function', 'arn:aws:lambda:us-east-1:123456789012:function:MyFn');

// THEN
expect(() => { fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); })
.toThrow(/Cannot modify permission to lambda function/);
expect(() => {
fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com'));
}).toThrow(/Cannot modify permission to lambda function/);
});

test('on an imported function (different account & w/ skipPermissions', () => {
// GIVEN
const stack = new cdk.Stack(undefined, undefined, {
env: { account: '111111111111' }, // Different account
});
const fn = lambda.Function.fromFunctionAttributes(stack, 'Function', {
functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn',
skipPermissions: true,
});

// THEN
expect(() => {
fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com'));
}).not.toThrow();
});
});

Expand Down