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

aws-lambda: Function.grantInvoke() not applying permissions for older versions of lambda function #32455

Open
1 task
michaellasmanis opened this issue Dec 10, 2024 · 7 comments
Assignees
Labels
@aws-cdk/aws-lambda Related to AWS Lambda bug This issue is a bug. needs-reproduction This issue needs reproduction. p2

Comments

@michaellasmanis
Copy link

michaellasmanis commented Dec 10, 2024

Describe the bug

when using Function.grantInvoke(), permission for function:Invoke is applied to the unqualified function arn but not to any of the older versions.

Regression Issue

  • Select this option if this issue appears to be a regression.

Last Known Working CDK Version

No response

Expected Behavior

Per the documentation here: https://docs.aws.amazon.com/cdk/api/v2/java/software/amazon/awscdk/services/lambda/package-summary.html#resource-based-policies-heading

and the GitHub PR here: #19318

I would expect that both the unqualified [FunctionArn] and [Function.Arn].* to have the permissions applied.

Current Behavior

Invoke permission only seem to be applied to the unqualified function arn.

cdk synth produces the following snippet:

  HelloWorldFunctionInvokeFcyXBRX02EWa52GlFECQiCzDt0fdRUDi4mo4foC5aU2283F0E1:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Fn::GetAtt:
          - HelloWorldFunctionB2AB6E79
          - Arn
      Principal: apigateway.amazonaws.com

which just enabled the unqualified ARN

Reproduction Steps

if you have a stack containing such:

    // Define the Lambda function resource
    const myFunction = new lambda.Function(this, "HelloWorldFunction", {
      runtime: lambda.Runtime.NODEJS_20_X, // Provide any supported Node.js runtime
      handler: "index.handler",
      code: lambda.Code.fromInline(`
        exports.handler = async function(event) {
          return {
            statusCode: 200,
            body: JSON.stringify('Hello World on ` + new Date().toLocaleString() + `!'),
          };
        };
      `),
    });

    const myVersion = new lambda.Version(this, "HelloWorldFunction" + new Date().getTime()/1000,
    {
      lambda: myFunction,
      removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
    });

    myFunction.grantInvoke(new iam.ServicePrincipal('apigateway.amazonaws.com'));

you will notice after the second (or subsequent) deploy that the permissions from the non-$LATEST versions are removed as part of the cdk deploy.

Possible Solution

No response

Additional Information/Context

No response

CDK CLI Version

2.172.0

Framework Version

bootstrap version 25

Node.js Version

v23.3.0

OS

macOS

Language

Java

Language Version

java 17

Other information

No response

@michaellasmanis michaellasmanis added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Dec 10, 2024
@github-actions github-actions bot added the @aws-cdk/aws-lambda Related to AWS Lambda label Dec 10, 2024
@michaellasmanis
Copy link
Author

sample project based on cdk init
hello-cdk-java.zip

@nmussy
Copy link
Contributor

nmussy commented Dec 10, 2024

Hey @michaellasmanis,

There definitely is a difference between granting invoke to a role vs a service. The former is applied directly to the function's policy document, and the latter is applied via a CfnPermission resource . This is the current behavior:

// GIVEN
const stack = new cdk.Stack();
const role = new iam.Role(stack, 'Role', {
  assumedBy: new iam.AccountPrincipal('1234'),
});
const service = new iam.ServicePrincipal('apigateway.amazonaws.com');
const fn = new lambda.Function(stack, 'Function', {
  code: lambda.Code.fromInline('xxx'),
  handler: 'index.handler',
  runtime: lambda.Runtime.NODEJS_LATEST,
});

// WHEN
fn.grantInvoke(role);
fn.grantInvoke(service);

// THEN
expect(fn.resourceArnsForGrantInvoke).toEqual([fn.functionArn, `${fn.functionArn}:*`]);
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'] }, ':*']] },
        ],
      },
    ],
  },
});
// There is only a single Permission resource
Template.fromStack(stack).resourceCountIs('AWS::Lambda::Permission', 1);
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', {
  Action: 'lambda:InvokeFunction',
  FunctionName: { 'Fn::GetAtt': ['Function76856677', 'Arn'] },
  Principal: 'apigateway.amazonaws.com',
});

It seems like you should be able to qualify CfnPermission.FunctionName, but I haven't looked too deeply into it yet

@michaellasmanis
Copy link
Author

@nmussy

Thanks for the clarification, that makes sense. However, there is then definitely a documentation error at least in the javadocs:
https://docs.aws.amazon.com/cdk/api/v2/java/software/amazon/awscdk/services/lambda/package-summary.html#resource-based-policies-heading

 ServicePrincipal principal = new ServicePrincipal("my-service");
 
 fn.grantInvoke(principal);

By default fn.grantInvoke() grants permission to the principal to invoke any version of the function, including all past ones. If you only want the principal to be granted permission to invoke the latest version or the unqualified Lambda ARN, use grantInvokeLatestVersion(grantee).

The example code from the javadocs is basically what I copied and pasted into my test case.

Basically, I'm trying to work around issue #28412. I'll check out CfnPermission.FunctionName to see if it can work for me. Any other suggestions are highly appreciated.

@nmussy
Copy link
Contributor

nmussy commented Dec 10, 2024

I think there's an issue with the current implementation of the internal grant method when given a service, as it seems the qualifier is being ignored even when explicitly set:

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');

fn.grantInvoke(service);
fn.grantInvokeLatestVersion(service);
fn.grantInvokeVersion(service, fn.latestVersion);

All 3 grants generate effectively the same policy:

[
    "FunctionInvokeFcyXBRX02EWa52GlFECQiCzDt0fdRUDi4mo4foC5aU730270F4": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "Action": "lambda:InvokeFunction",
        "FunctionName": {
          "Fn::GetAtt": [
            "Function76856677",
            "Arn"
          ]
        },
        "Principal": "apigateway.amazonaws.com"
      }
    },
    "FunctionInvokeKTHGXOMcvOrpWFkuCdKIjgLLIicmjeE3TRCaRos63A32ADB": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "Action": "lambda:InvokeFunction",
        "FunctionName": {
          "Fn::GetAtt": [
            "Function76856677",
            "Arn"
          ]
        },
        "Principal": "apigateway.amazonaws.com"
      }
    },
    "FunctionCurrentVersion4E2B2261ecf75ed18d6b6a0a5635c08929baf432": {
      "Type": "AWS::Lambda::Version",
      "Properties": {
        "FunctionName": {
          "Ref": "Function76856677"
        }
      }
    },
    "FunctionInvokewPiaqpkwMJl8Ed9zUwcfGrfRum0xC0KM1xqhfffIXlo2E3F6599": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "Action": "lambda:InvokeFunction",
        "FunctionName": {
          "Fn::GetAtt": [
            "Function76856677",
            "Arn"
          ]
        },
        "Principal": "apigateway.amazonaws.com"
      }
    }
  }
]

@michaellasmanis
Copy link
Author

michaellasmanis commented Dec 10, 2024

I agree, I would expect fn.grantInvokeVersion(service, fn.latestVersion) to create a qualified permission. I just tested with an explicitly created version and it had the same effect (ie, all 3 permissions the same):

    const fn = new lambda.Function(this, 'Function', {
      code: lambda.Code.fromInline('xxx'),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_LATEST,
    });
    const service = new iam.ServicePrincipal('apigateway.amazonaws.com');

    const myVersion = new lambda.Version(this, "HelloWorldFunction" + new Date().getTime()/1000,
    {
      lambda: fn
    });

    fn.grantInvoke(service);
    fn.grantInvokeLatestVersion(service);
    fn.grantInvokeVersion(service, myVersion);
  }

yielded:

  FunctionInvokeFcyXBRX02EWa52GlFECQiCzDt0fdRUDi4mo4foC5aU730270F4:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Fn::GetAtt:
          - Function76856677
          - Arn
      Principal: apigateway.amazonaws.com
    Metadata:
      aws:cdk:path: HelloCdkJavascriptStack/Function/InvokeFcyXBRX02EWa52GlF+ECQiCzDt0fdRUDi4mo4foC5aU=
  FunctionInvokeKTHGXOMcvOrpWFkuCdKIjgLLIicmjeE3TRCaRos63A32ADB:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Fn::GetAtt:
          - Function76856677
          - Arn
      Principal: apigateway.amazonaws.com
    Metadata:
      aws:cdk:path: HelloCdkJavascriptStack/Function/InvokeKTHGXOMcvOrpWFk--uCdKIjgLL+I--i--cmjeE3TRCaRos=
  FunctionInvokeAauQPlz8eN4vABc4hDU33BZEklppIj9DrYolf8yTv3kAB59B1A5:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Fn::GetAtt:
          - Function76856677
          - Arn
      Principal: apigateway.amazonaws.com
    Metadata:
      aws:cdk:path: HelloCdkJavascriptStack/Function/InvokeAauQPlz8eN4vABc4hDU33BZEklppIj9DrYolf8yTv3k=

also fn.grantInvokeVersion(service, fn.currentVersion)(should reference version qualified arn) behaves the same as fn.grantInvokeVersion(service, fn.latestVersion)(should reference $LATEST arn) which seems to not agree with the docs here:
https://docs.aws.amazon.com/cdk/api/v2/java/software/amazon/awscdk/services/lambda/package-summary.html#versions-heading

However, most AWS services require a specific AWS Lambda version, and won't allow you to use $LATEST. Therefore, you would normally want to use lambda.currentVersion.

@khushail khushail self-assigned this Dec 10, 2024
@khushail khushail added needs-reproduction This issue needs reproduction. p2 and removed needs-triage This issue or PR still needs to be triaged. labels Dec 10, 2024
@nmussy
Copy link
Contributor

nmussy commented Dec 11, 2024

I did a little more digging, and it seems the issue arises here:

// Fake resource-like object on which to call addToResourcePolicy(), which actually
// calls addPermission()
resource: {
addToResourcePolicy: (_statement) => {
// Couldn't add permissions to the principal, so add them locally.
this.addPermission(identifier, {
principal: grantee.grantPrincipal!,
action: action,
...permissionOverrides,
});

The IAM addToResourcePolicy method is overridden, and a CfnPermission is generated, but that resource is not given anything other than this.functionArn, regardless of whether or not the permission qualifies it:

new CfnPermission(scope, id, {
action,
principal,
functionName: this.functionArn,
eventSourceToken: permission.eventSourceToken,
sourceAccount: permission.sourceAccount ?? sourceAccount,
sourceArn: permission.sourceArn ?? sourceArn,
principalOrgId: permission.organizationId ?? principalOrgID,
functionUrlAuthType: permission.functionUrlAuthType,
});

I imagine we might run into some issues if we attempt to generate multiple CfnPermissions for a single grant, given CfnPermission.FunctionName is not an array

@michaellasmanis
Copy link
Author

michaellasmanis commented Dec 11, 2024

@nmussy

After looking at CfnPermission.FunctionName more closely, I agree. The validation rule for it will only accept a single ARN that is either unqualified or fully qualified (ie version or alias) but not a wildcard. So without explicitly creating a ChnPermission for each existing version/alias, I don't think there is any way to resolve my original issue.

However, I do think that the following two items should be addressed:

  1. update the overview documentation for grantInvoke*() to reflect the actual behavior (role vs principal and what gets created ie role/policy vs permission and scopes):
    https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda-readme.html#resource-based-policies
    and associated language specific docs (python/javadocs/etc)

  2. address the failure to qualify the CfnPermission generated by fn.grantInvokeLatestVersion(service) and fn.grantInvokeVersion(service, fn.latestVersion)

Michael

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-lambda Related to AWS Lambda bug This issue is a bug. needs-reproduction This issue needs reproduction. p2
Projects
None yet
Development

No branches or pull requests

3 participants