diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts index 0dcf4c581598c..1e58b82ca5790 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/experimental/edge-function.ts @@ -119,6 +119,9 @@ export class EdgeFunction extends Resource implements lambda.IVersion { public grantInvoke(identity: iam.IGrantable): iam.Grant { return this.lambda.grantInvoke(identity); } + public grantInvokeV2(identity: iam.IGrantable, grantVersionAccess?: boolean): iam.Grant { + return this.lambda.grantInvokeV2(identity, grantVersionAccess); + } public grantInvokeUrl(identity: iam.IGrantable): iam.Grant { return this.lambda.grantInvokeUrl(identity); } diff --git a/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts b/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts index 9e6055eb86b4b..875dce94f2820 100644 --- a/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts +++ b/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts @@ -95,12 +95,18 @@ export interface IFunction extends IResource, ec2.IConnectable, iam.IGrantable { /** * Grant the given identity permissions to invoke this Lambda */ - grantInvoke(identity: iam.IGrantable): iam.Grant; + grantInvoke(grantee: iam.IGrantable): iam.Grant; + + /** + * Grant the given identity permissions to invoke to $Latest version when grantVersionAccess is false + * Grant the given identity permissions to invoke All version when grantVersionAccess is true + */ + grantInvokeV2(grantee: iam.IGrantable, grantVersionAccess?: boolean): iam.Grant; /** * Grant the given identity permissions to invoke this Lambda Function URL */ - grantInvokeUrl(identity: iam.IGrantable): iam.Grant; + grantInvokeUrl(grantee: iam.IGrantable): iam.Grant; /** * Grant multiple principals the ability to invoke this Lambda via CompositePrincipal @@ -437,6 +443,39 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC return grant; } + /** + * Grants the specified identity permissions to invoke this Lambda function. + * + * **Important:** Avoid using `grantInvokeV2` in conjunction with `grantInvoke`. + * + * @param grantee The principal (identity) to grant invocation permission. + * @param grantVersionAccess (Optional) Controls whether to grant access to all function versions. Defaults to `false`. + * - When set to `false`, only the function without a specific version (`$Latest`) can be invoked. + * - When set to `true`, both the function and functions with specific versions can be invoked. + */ + public grantInvokeV2(grantee: iam.IGrantable, grantVersionAccess?: boolean): iam.Grant { + const hash = createHash('sha256') + .update(JSON.stringify({ + principal: grantee.grantPrincipal.toString(), + conditions: grantee.grantPrincipal.policyFragment.conditions, + grantVersionAccess: grantVersionAccess, + }), 'utf8') + .digest('base64'); + const identifier = `Invoke${hash}`; + + // Memoize the result so subsequent grantInvokeV2() calls are idempotent + let grant = this._invocationGrants[identifier]; + if (!grant) { + let resouceArns = [this.functionArn]; + if (grantVersionAccess) { + resouceArns = this.resourceArnsForGrantInvoke; + } + grant = this.grant(grantee, identifier, 'lambda:InvokeFunction', resouceArns); + this._invocationGrants[identifier] = grant; + } + return grant; + } + /** * Grant the given identity permissions to invoke this Lambda Function URL */ diff --git a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts index f4c5382707641..3e9c71d321cf6 100644 --- a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts +++ b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts @@ -1525,6 +1525,334 @@ describe('function', () => { }); }); + describe('grantInvokeV2', () => { + test('adds iam:InvokeFunction', () => { + // GIVEN + const stack = new cdk.Stack(); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountPrincipal('1234'), + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + + // WHEN + fn.grantInvokeV2(role); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Function76856677', 'Arn'] }, + }, + ], + }, + }); + }); + + test('adds iam:InvokeFunction with version', () => { + // GIVEN + const stack = new cdk.Stack(); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AccountPrincipal('1234'), + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + + // WHEN + fn.grantInvokeV2(role, true); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [ + { 'Fn::GetAtt': ['Function76856677', 'Arn'] }, + { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['Function76856677', 'Arn'] }, ':*']] }, + ], + }, + ], + }, + }); + }); + + test('with a service principal', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + const service = new iam.ServicePrincipal('apigateway.amazonaws.com'); + + // WHEN + fn.grantInvokeV2(service); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'apigateway.amazonaws.com', + }); + }); + + test('with an account principal', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + const account = new iam.AccountPrincipal('123456789012'); + + // WHEN + fn.grantInvokeV2(account); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: '123456789012', + }); + }); + + test('with an arn principal', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + const account = new iam.ArnPrincipal('arn:aws:iam::123456789012:role/someRole'); + + // WHEN + fn.grantInvokeV2(account); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'arn:aws:iam::123456789012:role/someRole', + }); + }); + + test('with an organization principal', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + const org = new iam.OrganizationPrincipal('my-org-id'); + + // WHEN + fn.grantInvokeV2(org); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: '*', + PrincipalOrgID: 'my-org-id', + }); + }); + + test('can be called twice for the same service principal', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + const service = new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com'); + + // WHEN + fn.grantInvokeV2(service); + fn.grantInvokeV2(service); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'elasticloadbalancing.amazonaws.com', + }); + }); + + test('with an imported role (in the same account)', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { account: '123456789012' }, + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + + // WHEN + fn.grantInvokeV2(iam.Role.fromRoleArn(stack, 'ForeignRole', 'arn:aws:iam::123456789012:role/someRole')); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Function76856677', 'Arn'] }, + }, + ], + }, + Roles: ['someRole'], + }); + }); + + test('with an imported role (from a different account)', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { account: '3333' }, + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('xxx'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + + // WHEN + fn.grantInvokeV2(iam.Role.fromRoleArn(stack, 'ForeignRole', 'arn:aws:iam::123456789012:role/someRole')); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'arn:aws:iam::123456789012:role/someRole', + }); + }); + + test('on an imported function (same account)', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { account: '123456789012' }, + }); + const fn = lambda.Function.fromFunctionArn(stack, 'Function', 'arn:aws:lambda:us-east-1:123456789012:function:MyFn'); + + // WHEN + fn.grantInvokeV2(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + Principal: 'elasticloadbalancing.amazonaws.com', + }); + }); + + test('on an imported function (unresolved account)', () => { + const stack = new cdk.Stack(); + const fn = lambda.Function.fromFunctionArn(stack, 'Function', 'arn:aws:lambda:us-east-1:123456789012:function:MyFn'); + + expect( + () => fn.grantInvokeV2(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')), + ).toThrow(/Cannot modify permission to lambda function/); + }); + + test('on an imported function (unresolved account & w/ allowPermissions)', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn = lambda.Function.fromFunctionAttributes(stack, 'Function', { + functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + sameEnvironment: true, + }); + + // WHEN + fn.grantInvokeV2(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + Principal: 'elasticloadbalancing.amazonaws.com', + }); + }); + + test('on an imported function (different account)', () => { + // GIVEN + const stack = new cdk.Stack(undefined, undefined, { + env: { account: '111111111111' }, // Different account + }); + const fn = lambda.Function.fromFunctionArn(stack, 'Function', 'arn:aws:lambda:us-east-1:123456789012:function:MyFn'); + + // THEN + expect(() => { + fn.grantInvokeV2(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.grantInvokeV2(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + }).not.toThrow(); + }); + }); + describe('grantInvokeCompositePrincipal', () => { test('adds iam:InvokeFunction for a CompositePrincipal (two accounts)', () => { // GIVEN