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

feat(lambda): add grantInvokeLatestVersion to grant invoke only to latest function version #29856

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

roger-zhangg
Copy link
Member

@roger-zhangg roger-zhangg commented Apr 16, 2024

Issue # (if applicable)

Closes #20177

Reason for this change

fn.grantInvoke() will grant invoke permission to invoke both the latest version and all pervious version of the lambda function. We can see this behavior could bring some security concern for some of our customers.

Description of changes

We provides a new function fn.grantInvokeLatestVersion() to grant invoke only to the Latest version of function and the unqualified lambda arn

Example:

// Grant permissions to a service
declare const fn: lambda.Function;
const principal = new iam.ServicePrincipal('my-service');

fn.grantInvokeLatestVersion(principal);

Description of how you validated changes

Added unit tests and integration tests.
When using fn.grantInvokeLatestVersion() granted principle to invoke a function's past version, it will get the following error:

An error occurred (AccessDeniedException) when calling the Invoke operation: User: {$principle} is not authorized to perform: lambda:InvokeFunction on resource: {$LambdaArn:$version} because no identity-based policy allows the lambda:InvokeFunction action

Alternative design (to discuss)

setup a grantInvokeProp including grantVersionAccess flag to pass in the grantInvokeLatestVersion instead using grantVersionAccess flag directly on grantInvokeLatestVersion
-> This is discussed in the comments, I agree having props will have future extensibility but usually for grant methods specifically we haven't seen before. So we will not add prop to the new function grantInvokeLatestVersion

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@github-actions github-actions bot added repeat-contributor [Pilot] contributed between 3-5 PRs to the CDK effort/small Small work item – less than a day of effort feature-request A feature should be added or improved. p1 labels Apr 16, 2024
@aws-cdk-automation aws-cdk-automation requested a review from a team April 16, 2024 17:59
Copy link
Collaborator

@aws-cdk-automation aws-cdk-automation left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pull request linter has failed. See the aws-cdk-automation comment below for failure reasons. If you believe this pull request should receive an exemption, please comment and provide a justification.

A comment requesting an exemption should contain the text Exemption Request. Additionally, if clarification is needed add Clarification Request to a comment.

@roger-zhangg roger-zhangg changed the title (lambda): add grantInvokeV2 to grant for only latest version feat(lambda): add grantInvokeV2 to grant invoke only to latest function version Apr 16, 2024
@aws-cdk-automation aws-cdk-automation dismissed their stale review April 16, 2024 22:08

✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.

@roger-zhangg roger-zhangg marked this pull request as ready for review April 16, 2024 22:25
@aws-cdk-automation aws-cdk-automation added the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Apr 16, 2024
Copy link
Contributor

@nmussy nmussy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A cleaner way to implement this feature would be to extend the current implementation of grantInvoke and to add a onlyGrantLatestVersion parameter with a default value. Using a Props object is also preferable for future extensibility:

interface GrantInvokeProps {
  onlyGrantLatestVersion?: boolean;
}
grantInvoke(grantee: iam.IGrantable, { onlyGrantLatestVersion = false }: GrantInvokeProps = {}): iam.Grant;

This would not cause a breaking change, given existing calls to grantInvoke would remain correct and unchanged.

EDIT: Sorry, I didn't notice you proposed the same solution at the end of your PR comment. I would definitely favor it

@godwingrs22 godwingrs22 self-requested a review April 17, 2024 17:49
@godwingrs22 godwingrs22 self-assigned this Apr 17, 2024
const hash = createHash('sha256')
.update(JSON.stringify({
principal: grantee.grantPrincipal.toString(),
conditions: grantee.grantPrincipal.policyFragment.conditions,
onlyGrantLatestVersion: props.onlyGrantLatestVersion,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching between grantInvoke(grantee) and grantInvoke(grantee, { onlyGrantLatestVersion: false }) would cause an unnecessary hash change

Suggested change
onlyGrantLatestVersion: props.onlyGrantLatestVersion,
onlyGrantLatestVersion: props.onlyGrantLatestVersion ?? false,

@@ -201,6 +201,15 @@ You can also restrict permissions given to AWS services by providing
a source account or ARN (representing the account and identifier of the resource
that accesses the function or layer).

**Important**: 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 invoke the latest version, use `grantInvoke(grantee, { onlyGrantLatestVersion:true })`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Important**: 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 invoke the latest version, use `grantInvoke(grantee, { onlyGrantLatestVersion:true })`.
**Important**: 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, use `grantInvoke(grantee, { onlyGrantLatestVersion: true })`.

You can also use a Markdown quote to increase the visibility, e.g. this note

> By default...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add unit tests with imported Lambda functions as well? I am especially anticipating issues with the following:

lambda.Function.fromFunctionArn(
  stack,
  'unqualified',
  'arn:aws:lambda:us-east-1:123456789012:function:my-function'
);

lambda.Function.fromFunctionArn(
  stack,
  'v1',
  'arn:aws:lambda:us-east-1:123456789012:function:my-function:1'
);

lambda.Function.fromFunctionArn(
  stack,
  'latest',
  'arn:aws:lambda:us-east-1:123456789012:function:my-function:$LATEST'
);

If you onlyGrantLatestVersion for the first example, I would assume it would actually only grant the ARN version, which might not actually be the latest version

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the hint, and this is interesting. For the current design onlyGrantLatestVersion will grant permission to this.functionArn. While the previous design without onlyGrantLatestVersion grants to [this.functionArn, ${this.functionArn}:*]. I would assume if this.functionArn is something like arn:aws:lambda:us-east-1:123456789012:function:my-function:1 the previous method won't work either. I'll update once I checked this out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I tried out fromFunctionArn and seems CDK doesn't allow calling grantInvoke on a imported Lambda function

sample code

const func2 = lambda.Function.fromFunctionArn(
  this,
  'v1',
  'arn:aws:lambda:us-east-1:123456789012:function:my-function:1'
);

const lambdaRole = new iam.Role(this, 'LambdaRole', {
  assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});

func2.grantInvoke(lambdaRole);

error

/local/home/ruojiazh/proj/aws-cdk/packages/aws-cdk-lib/aws-lambda/lib/function-base.ts:563
            throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version.\n'
                  ^
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 the function is imported from a different account and already has the correct permissions use `fromFunctionAttributes()` API with the `skipPermissions` flag.

Using the suggested method fromFunctionAttributes

    const func2 = lambda.Function.fromFunctionAttributes(this, 'Function', {
      functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:my-function:1',
      sameEnvironment: true,
    });


    const lambdaRole = new iam.Role(this, 'LambdaRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });

    func2.grantInvoke(lambdaRole);

output

PolicyDocument:
        Statement:
          - Action: lambda:InvokeFunction
            Effect: Allow
            Resource:
              - arn:aws:lambda:us-east-1:123456789012:function:my-function:1
              - arn:aws:lambda:us-east-1:123456789012:function:my-function:1:*
        Version: "2012-10-17"

You can see the output here is actually wrong, However because it still have allow on arn:aws:lambda:us-east-1:123456789012:function:my-function:1, this will allow the principle to invoke the given version. I assume CDK doesn't expect you to passin the function arn with a version in fromFunctionAttributes and CDK also didn't (or unable to) check if this ARN is valid or not. If we follow this design, we can also assume the Arn passed in shouldn't have a version.

grant = this.grant(grantee, identifier, 'lambda:InvokeFunction', this.resourceArnsForGrantInvoke);
let resouceArns = this.resourceArnsForGrantInvoke;
if (props.onlyGrantLatestVersion) {
resouceArns = [this.functionArn];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is functionArn always fully qualified? If it is, what happens if functionArn points to a specific version, e.g. my-function:1, but other versions were published since? You are no longer granting the latest version, just "a" version

See the comment on the unit tests file below

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to the comment above, the previous design doesn't expect a functionArn with version

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The integration feels a bit lackluster here, we're only testing that the policy is synthetically correct, not that the grants work as expected

Adding integration assertions to check that the functions are invokable with the provided examples would be ideal, either with AwsApiCalls, or by adding an API Gateway to integrate the Lambdas and running HttpApiCalls

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree only testing synthetically is kind of wired, but doing AwsApiCalls seems to be testing against Lambda/IAM rather than testing CDK. So I just followed the previous test behavior

/**
* Controls whether to grant invoke access to all function versions. Defaults to `false`.
* - When set to `false`, both the function and functions with specific versions can be invoked.
* - When set to `true`, only the function without a specific version (`$Latest`) can be invoked.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this accurate? I would imagine that at least functionname:$LATEST would be invokable, and probably even functionname:3, if 3 was the latest published version

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this, I think you are right👍

@@ -95,12 +95,12 @@ 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, props?: GrantInvokeProps): iam.Grant;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we add the props to the existing grantInvoke method which differs from the description of this PR. Are we not creating as new helper method grantInvokeV2 with the props without touching the existing method?

We provides a new function fn.grantInvokeV2() with a flag grantVersionAccess to controls whether to grant access to all function versions. Defaults to false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the propsed solution in updating the existing method grantInvoke with optional props, can we also update the PR description and update in the original issue?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updating the existing parameter name identity would it cause a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also confused on this part, this interface uses identity as param, however the implementation uses grantee. I'm not sure if we should just keep it as is or should we change one to match the other?

public grantInvoke(identity: iam.IGrantable): iam.Grant {
return this.lambda.grantInvoke(identity);
public grantInvoke(identity: iam.IGrantable, props?: lambda.GrantInvokeProps): iam.Grant {
return this.lambda.grantInvoke(identity, props);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need to update the identity parameter to grantee as defined in the lambda grantInvoke method?

@@ -17,6 +17,10 @@ fn.grantInvoke(new iam.AnyPrincipal().inOrganization('o-yyyyyyyyyy'));

fn.grantInvoke(new iam.OrganizationPrincipal('o-xxxxxxxxxx'));

fn.grantInvoke(new iam.AnyPrincipal().inOrganization('o-yyyyyyyyyy2'), { onlyGrantLatestVersion: true });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we able to assert that onlyGrantLatestVersion: true grants only to the specific latest version and other versions should get an error on invocation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @godwingrs22 I'm looking into this but could you give me a headup on how to write integ test on this? I think I need to assume to the role I created here. But I didn't find how can I assume role in integration test. I checked invokeFucntion here and invokefunctionprop here but nothing related to assume role is found.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a inspiration on this from my teammate:

Setting up integration test

I can setup 2 lambda function - fn1, fn2. And a role in the integ-test.
fn1 should have multiple versions, and grants invoke lastest version to role
fn2 uses this role, and the handler of fn2 will invoke fn1 with the given version then return the result

running the integration test

In the integration test, we will only invoke fn2 by integ.assertions.invokeFunction(fn2, version). In this way we will know if the role attached to fn2 can invoke fn1 or not. That is:

When

fn1.grantInvokeLatestVersion(role)

Then
integ.assertions.invokeFunction(fn2, '$LATEST') should success
integ.assertions.invokeFunction(fn2, 1) should fail

When
fn1.grantInvokeVersion(role, 1)

Then
integ.assertions.invokeFunction(fn2, '$LATEST') should fail
integ.assertions.invokeFunction(fn2, 1) should success

What do you think?

@godwingrs22
Copy link
Member

  1. Instead of adding props to the existing grantInvoke method can we create new method instead as stated in the description? Because I saw most of our grant methods in other modules accepts only one parameter grantee and doesn't take props as a param. Hence we propose to follow the similar approach.

Examples:
grantReadData(grantee) in TableV2 Construct of DynamoDB
grantManageConnections(identity) in WebSocketApi construct of apigateway

  1. Also for naming the new method, we suggest not to have V2 in the method name and have something like grantInvokeLatestVersion() or grantInvokeOnlyLatestVersion()

@roger-zhangg
Copy link
Member Author

roger-zhangg commented Apr 22, 2024

Hi @godwingrs22 thanks for the review, I can see your point. For the naming, If we name the new function like grantInvokeLatestVersion() Then maybe we don't need to provide props to the new function. But this also eliminates possible extensibility. What do you think?

@godwingrs22
Copy link
Member

Hi @roger-zhangg, Yes with new method approach we don't need to provide props. I agree having props will have future extensibility but usually for grant methods specifically we haven't seen before.

grantInvokeLatestVersion(grantee: iam.IGrantable)

@roger-zhangg roger-zhangg changed the title feat(lambda): add grantInvokeV2 to grant invoke only to latest function version feat(lambda): add grantInvokeLatestVersion to grant invoke only to latest function version Apr 22, 2024
@roger-zhangg
Copy link
Member Author

Hi @godwingrs22 should we consider supporting granting invoke to a certain version? Could be something like grantInvokeVersion(grantee: iam.IGrantable, version?: IVersion) and defaults to $LATEST

@nmussy
Copy link
Contributor

nmussy commented Apr 24, 2024

A good compromise might be to have both grantInvokeLatestVersion(grantee: iam.IGrantable) and grantInvokeVersion(grantee: iam.IGrantable, version: IVersion), with the former calling the latter. We'd get both the more commonly used $LATEST grant ,and the flexibility of pinpointing a specific version

@roger-zhangg
Copy link
Member Author

roger-zhangg commented May 1, 2024

Hi @nmussy , @godwingrs22 I'm really grateful for your great and helpful ideas, I have updated and replied to some of the conversation above. Please help to review so I can proceed and merge it sooner. Thanks!

In my opinion, it's hard to simulate the whole scenario in the integration test.
1: as stated above, there's no standard way to assume role to invoke function in the current integration test
2: CDK/CFN can't deploy multiple versions in one deployments

Given these facts, I think it's reasonable to just test if the permission is correctly set, rather than actually invoking the Lambda function with version.

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildv2Project1C6BFA3F-wQm2hXv2jqQv
  • Commit ID: 36ee9a9
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@roger-zhangg
Copy link
Member Author

Hi @godwingrs22 , could you please help to take a look when available, Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
effort/small Small work item – less than a day of effort feature-request A feature should be added or improved. p1 pr/needs-maintainer-review This PR needs a review from a Core Team Member repeat-contributor [Pilot] contributed between 3-5 PRs to the CDK
Projects
None yet
Development

Successfully merging this pull request may close these issues.

(lambda): Function.grant_invoke should not grant for all versions
4 participants