aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.55k stars 3.87k forks source link

Cognito Construct: Add grant* methods #7112

Open 0xdevalias opened 4 years ago

0xdevalias commented 4 years ago

As per https://github.com/aws/aws-cdk/issues/6765#issuecomment-607050027, the UserPool construct should have grant* methods on it to give other resources (eg. lambda functions) access to various API/SDK methods.

Use Case

I want to be able to easily give my lambda functions access to call AWS API/SDK methods against my UserPool.

Proposed Solution

References ### CDK - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-cognito.UserPool.html - https://docs.aws.amazon.com/cdk/api/latest/docs/aws-iam-readme.html - https://docs.aws.amazon.com/cdk/latest/guide/permissions.html ### Cognito - https://docs.aws.amazon.com/cognito/latest/developerguide/resource-permissions.html - https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/Welcome.html - eg. https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminGetUser.html - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html - eg. https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#adminGetUser-property

Based on:

eg.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1585721272022",
      "Action": [
        "cognito-idp:AdminDisableUser",
        "cognito-idp:AdminEnableUser",
        "cognito-idp:AdminGetUser"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}"
    }
  ]
}

Workaround:

import { UserPool } from '@aws-cdk/aws-cognito'
import { Effect, PolicyStatement } from '@aws-cdk/aws-iam'

// ..snip..

    /**
     * Lookup authentication UserPool
     */
    const userPool = UserPool.fromUserPoolId(this, 'UserPool', userPoolId)

// ..snip..

    fnHandler.addToRolePolicy(
      new PolicyStatement({
        effect: Effect.ALLOW,
        actions: [
          'cognito-idp:AdminGetUser',
          'cognito-idp:AdminEnableUser',
          'cognito-idp:AdminDisableUser',
          // etc
        ],
        resources: [
          `arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}`,
        ],
      })
    )

Other


This is a :rocket: Feature Request

nija-at commented 4 years ago

@0xdevalias -

I want to be able to easily give my lambda functions access to call AWS API/SDK methods against my UserPool.

Can you give more details on your use case? What do these lambda functions do?

I am looking to see how we can organize these grant methods, and it looks like Cognito Identity Provider has quite a number of APIs. As an example, we have a number of grant methods organized by use case for S3 buckets, similar for Lambda functions and DynamoDB tables.

0xdevalias commented 4 years ago

As per the example code in my original post, they’re needing to use the API calls, in this case for a number of the ‘admin*’ methods in Cognito User Pools.

Even if it was just a single grant method that had types that made it easy to add any of the valid methods (eg, array of enum type thing)

pszabop commented 4 years ago

Can you give more details on your use case? What do these lambda functions do?

I just figured this out too and got an answer from Gitter before finding this.

My use case is "a user wants to grant another user permission to do something". In order to do that the other user has to be found using Cognito API calls in a lambda function. Once the user is found, update dynamodb with the relevant permission and ID of the other user.

aaronaustin commented 3 years ago

I think this explains why I've been getting a permission error on my lambda while trying to use AdminResetUserPassword. I'm setting a policy in CDK like this:

    const poolPolicyAddendum = new PolicyStatement({
      resources: [pool.userPoolArn],
      effect: Effect.ALLOW,
      actions: [
        'cognito-idp:List*',
        'cognito-idp:Write*',
        'cognito-idp:AdminResetUserPassword',
      ],
    });

I'm still getting an access denied error though. Is this the same issue? Has there been any progress on this or other solutions? I tried posting in stackoverflow, but nothing yet. Glad I found this. Thanks!

douglasnaphas commented 3 years ago

Here is my use case.

I have a Lambda Function that is the handler for a serverless web backend.

When the backend Lambda Function is invoked, I want it to be able to call cognito-idp:DescribeUserPoolClient to get the User Pool's client secret, so that it can POST to the TOKEN endpoint to exchange the authorization code issued by Cognito for JWT tokens.

Without the required permission, my Lambda Function is logging:

AccessDeniedException: User: arn:aws:sts::<ACCOUNT ID>:assumed-role/<REST OF THE ROLE ID> is not authorized to perform: cognito-idp:DescribeUserPoolClient on resource: arn:aws:cognito-idp:<REGION>:<ACCOUNT ID>:userpool/<USER POOL ID>
TomBonnerAtDerivitec commented 3 years ago

Yep, our use-case is a fairly obvious one really. When we create a lambda to execute off the PreSignUp Lambda Trigger we'd like it to be able to access the UserPool to validate things. You can see people working around this by manually adding to Role Policies here and even your own documentation explains how to work around it here. Many thanks for looking into this.

ShivamJoker commented 2 years ago

Why this feature has not been added yet :/ Even after paying people to write code seems like lot of work.

Now I will have to create a new IAM policy and pass it to lambda so that it can access my user pool.

It really sad @aws that these important features are not getting added even after years

AndrewG-wf commented 2 years ago

Thanks for the workaround @0xdevalias, this was an incredibly frustrating issue to try and work through. After 2 years could this maybe get actioned?

derdeka commented 1 year ago

Not sure if this ticket is outdated or if it has been implemented in the meantime, but this works for me: userPool.grant(fnHandler, 'cognito-idp:AdminGetUser', 'cognito-idp:AdminEnableUser', 'cognito-idp:AdminDisableUser');

0xdevalias commented 1 year ago

Not sure if this ticket is outdated or if it has been implemented in the meantime

In CDK v1.202.0, I'm doing the following, which returns an IUserPool, which doesn't have any grant methods defined on it:

const userPool = UserPool.fromUserPoolId(this, 'UserPool', userPoolId)

I can see that the UserPool construct does have a grant method though:

github-actions[bot] commented 3 months ago

This issue has received a significant amount of attention so we are automatically upgrading its priority. A member of the community will see the re-prioritization and provide an update on the issue.

0xdevalias commented 3 months ago

In CDK v1.202.0, I'm doing the following, which returns an IUserPool, which doesn't have any grant methods defined on it

It seems like CDK 2.x has a grant method on the IUserPool, at least according to these docs:

mazyu36 commented 3 months ago

Hi.

Does anyone have any use cases where the grant method is needed, along with supporting documentation? I was thinking of addressing this issue, but I'm not quite sure what methods and permissions are required.​​​​​​​​​​​​​​​​

0xdevalias commented 3 months ago

I've just looked up the codebase that I originally raised this for:

class CustomNodeLambdaEventHandler extends Construct (wraps NodejsFunction) ```typescript import * as path from 'path' import { Construct, Duration } from '@aws-cdk/core' import { LambdaFunction as LambdaFunctionTarget } from '@aws-cdk/aws-events-targets' import { Runtime } from '@aws-cdk/aws-lambda' import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs' export interface CustomNodeLambdaEventHandlerProps { handlerPath: string handlerExport: string handlerDescription?: string } /** * Custom construct to simplify making NodeJS lambda EventBridge handlers * * @see https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_author */ export class CustomNodeLambdaEventHandler extends Construct { public readonly fnHandler: NodejsFunction public readonly ruleTarget: LambdaFunctionTarget public addEnvironment = (key: string, value: string) => this.fnHandler.addEnvironment(key, value) constructor( scope: Construct, id: string, props: CustomNodeLambdaEventHandlerProps ) { super(scope, id) const { handlerPath, handlerExport, handlerDescription } = props /** * Lambda NodeJS EventBridge handler * * @see https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-nodejs-readme.html * @see https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-nodejs-readme.html#configuring-parcel * @see https://parceljs.org/ */ this.fnHandler = new NodejsFunction(this, 'Handler', { description: handlerDescription, entry: path.join( __dirname, '..', 'functions', 'event-handlers', handlerPath ), handler: handlerExport, runtime: Runtime.NODEJS_12_X, memorySize: 128, timeout: Duration.seconds(60), }) /** * EventBridge Rule Target */ this.ruleTarget = new LambdaFunctionTarget(this.fnHandler) } } ```
FooEventStack ```typescript import { Construct, Stack, StackProps } from '@aws-cdk/core' import { UserPool } from '@aws-cdk/aws-cognito' import { EventBus, Rule } from '@aws-cdk/aws-events' import { Effect, PolicyStatement } from '@aws-cdk/aws-iam' import { StringParameter } from '@aws-cdk/aws-ssm' import { CustomNodeLambdaEventHandler } from './constructs/CustomNodeLambdaEventHandler' export interface FooEventStackProps extends StackProps { environment: string fooProductId: string fooAppUrl: string userPoolId: string stripeUserGroupName: string } export class FooEventStack extends Stack { public get stripeEventBusName(): string { return this.stripeEventBus.eventBusName } private readonly stripeEventBus: EventBus constructor(scope: Construct, id: string, props: FooEventStackProps) { super(scope, id, props) const { environment, fooProductId, fooAppUrl, userPoolId, stripeUserGroupName, } = props // Validate required stack props if (!environment) throw new Error('missing required stack prop: environment') if (!fooProductId) throw new Error('missing required stack prop: fooProductId') if (!fooAppUrl) throw new Error('missing required stack prop: fooAppUrl') if (!userPoolId) throw new Error('missing required stack prop: userPoolId') if (!stripeUserGroupName) throw new Error('missing required stack prop: stripeUserGroupName') /** * Stripe Secret API Key * * @see https://stripe.com/docs/keys */ const stripeSecretApiKey = StringParameter.fromStringParameterAttributes( this, 'StripeSecretApiKey', { parameterName: `/FOO/${environment}/stripe/secret_api_key`, } ).stringValue /** * Lookup authentication UserPool */ const userPool = UserPool.fromUserPoolId(this, 'UserPool', userPoolId) /** * EventBridge EventBus for Stripe webhook events */ this.stripeEventBus = new EventBus(this, 'StripeEventBus', { eventBusName: `${id}-stripe`, }) /** * EventBridge rule for Stripe customer.* events * * @see https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events.Rule.html * @see https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events.EventPattern.html * @see https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events-targets.LambdaFunction.html * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html */ const stripeCustomerEventsRule = new Rule( this, 'StripeCustomerEventsRule', { eventBus: this.stripeEventBus, description: 'FnStripeCustomerEventsHandler rule', eventPattern: { account: [this.account], region: [this.region], source: ['Stripe'], detailType: ['customer.created', 'customer.updated'], }, } ) /** * EventBridge handler for Stripe customer.* events */ const fnStripeCustomerEventsHandler = new CustomNodeLambdaEventHandler( this, 'FnStripeCustomerEventsHandler', { handlerPath: 'stripe/customerEventsHandler.ts', handlerExport: 'customerEventsHandler', handlerDescription: 'EventBridge handler for Stripe customer.* events', } ) fnStripeCustomerEventsHandler.addEnvironment( 'fooProductId', fooProductId ) fnStripeCustomerEventsHandler.addEnvironment( 'userPoolId', userPool.userPoolId ) fnStripeCustomerEventsHandler.addEnvironment( 'stripeUserGroupName', stripeUserGroupName ) // see https://github.com/aws/aws-cdk/issues/7112 fnStripeCustomerEventsHandler.fnHandler.addToRolePolicy( new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'cognito-idp:AdminGetUser', 'cognito-idp:AdminCreateUser', 'cognito-idp:AdminAddUserToGroup', 'cognito-idp:AdminEnableUser', ], resources: [ `arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}`, ], }) ) stripeCustomerEventsRule.addTarget(fnStripeCustomerEventsHandler.ruleTarget) /** * EventBridge rule for Stripe customer.subscription.* events * * @see https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events.Rule.html * @see https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events.EventPattern.html * @see https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events-targets.LambdaFunction.html * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/CloudWatchEventsandEventPatterns.html */ const stripeCustomerSubscriptionEventsRule = new Rule( this, 'StripeCustomerSubscriptionEventsRule', { eventBus: this.stripeEventBus, description: 'FnStripeCustomerSubscriptionEventsHandler rule', eventPattern: { account: [this.account], region: [this.region], source: ['Stripe'], detailType: [ 'customer.subscription.added', 'customer.subscription.updated', 'customer.subscription.deleted', ], }, } ) /** * EventBridge handler for Stripe customer.subscription.* events */ const fnStripeCustomerSubscriptionEventsHandler = new CustomNodeLambdaEventHandler( this, 'FnStripeCustomerSubscriptionEventsHandler', { handlerPath: 'stripe/customerSubscriptionEventsHandler.ts', handlerExport: 'customerSubscriptionEventsHandler', handlerDescription: 'EventBridge handler for Stripe customer.subscription.* events', } ) fnStripeCustomerSubscriptionEventsHandler.addEnvironment( 'stripeApiKey', stripeSecretApiKey ) fnStripeCustomerSubscriptionEventsHandler.addEnvironment( 'stripeAppUrl', fooAppUrl ) fnStripeCustomerSubscriptionEventsHandler.addEnvironment( 'fooProductId', fooProductId ) fnStripeCustomerSubscriptionEventsHandler.addEnvironment( 'userPoolId', userPool.userPoolId ) fnStripeCustomerSubscriptionEventsHandler.addEnvironment( 'stripeUserGroupName', stripeUserGroupName ) // see https://github.com/aws/aws-cdk/issues/7112 fnStripeCustomerSubscriptionEventsHandler.fnHandler.addToRolePolicy( new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'cognito-idp:AdminGetUser', 'cognito-idp:AdminCreateUser', 'cognito-idp:AdminAddUserToGroup', 'cognito-idp:AdminEnableUser', 'cognito-idp:AdminDisableUser', ], resources: [ `arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}`, ], }) ) stripeCustomerSubscriptionEventsRule.addTarget( fnStripeCustomerSubscriptionEventsHandler.ruleTarget ) } } ```

In FooEventStack we have some EventBridge EventBus Rules that trigger NodejsFunction lambda handlers to process Stripe events. We need to give these lambda handlers access to administer a Cognito UserPool that's created in another stack, and loaded here with UserPool.fromUserPoolId, which returns IUserPool.

These are the relevant snippets of code from the above that are granting the permissions:

fnStripeCustomerEventsHandler.fnHandler.addToRolePolicy(
  new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [
      'cognito-idp:AdminGetUser',
      'cognito-idp:AdminCreateUser',
      'cognito-idp:AdminAddUserToGroup',
      'cognito-idp:AdminEnableUser',
    ],
    resources: [
      `arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}`,
    ],
  })
)
fnStripeCustomerSubscriptionEventsHandler.fnHandler.addToRolePolicy(
  new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [
      'cognito-idp:AdminGetUser',
      'cognito-idp:AdminCreateUser',
      'cognito-idp:AdminAddUserToGroup',
      'cognito-idp:AdminEnableUser',
      'cognito-idp:AdminDisableUser',
    ],
    resources: [
      `arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}`,
    ],
  })
)

Edit: From a quick skim of the docs, it looks like instead of manually constructing the UserPool ARN like I did there (in the resources section), I probably could have just used userPool.userPoolArn

fnStripeCustomerEventsHandler / fnStripeCustomerSubscriptionEventsHandler are instances of my CustomNodeLambdaEventHandler, and the .fnHandler accesses the underlying NodejsFunction instance.

This code was written in AWS CDK 1.x days, where the IUserPool had no grant method:

Which has changed in 2.x, and is now available:

Looking at NodeJsFunction, we can see that it implements IGrantable:

Therefore in CDK 2.x, I believe we should be able to simplify my above code to the following (though I haven't tested this to be certain):

  const userPool = UserPool.fromUserPoolId(this, 'UserPool', userPoolId)

  // ..snip..

- fnStripeCustomerEventsHandler.fnHandler.addToRolePolicy(
-   new PolicyStatement({
-     effect: Effect.ALLOW,
-     actions: [
+ userPool.grant(fnStripeCustomerEventsHandler.fnHandler, [ 
        'cognito-idp:AdminGetUser',
        'cognito-idp:AdminCreateUser',
        'cognito-idp:AdminAddUserToGroup',
        'cognito-idp:AdminEnableUser',
+ ])
-     ],
-     resources: [
-       `arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}`,
-     ],
-   })
- )

  // ..snip..

- fnStripeCustomerSubscriptionEventsHandler.fnHandler.addToRolePolicy(
-   new PolicyStatement({
-     effect: Effect.ALLOW,
-     actions: [
+ userPool.grant(fnStripeCustomerSubscriptionEventsHandler.fnHandler, [ 
        'cognito-idp:AdminGetUser',
        'cognito-idp:AdminCreateUser',
        'cognito-idp:AdminAddUserToGroup',
        'cognito-idp:AdminEnableUser',
        'cognito-idp:AdminDisableUser',
+ ])
-     ],
-     resources: [
-       `arn:aws:cognito-idp:${userPool.stack.region}:${userPool.stack.account}:userpool/${userPool.userPoolId}`,
-     ],
-   })
- )

I'm not sure if there is as succinct a method of doing it in reverse, if you wanted to start with the NodeJsFunction and call functions directly on it to grant access to the UserPool; but perhaps that isn't the mental model CDK would follow anyway. The closest functions I could see for doing it that way (from a quick skim of the docs) seem to be:

So, at least for my original issue that I raised this for, I think CDK 2.x already implements what I would have needed/been asking for.

0xdevalias commented 3 months ago

Does anyone have any use cases where the grant method is needed, along with supporting documentation?

So, at least for my original issue that I raised this for, I think CDK 2.x already implements what I would have needed/been asking for.

Unless the question is more related to this earlier question:

Can you give more details on your use case? What do these lambda functions do?

I am looking to see how we can organize these grant methods, and it looks like Cognito Identity Provider has quite a number of APIs. As an example, we have a number of grant methods organized by use case for S3 buckets, similar for Lambda functions and DynamoDB tables.

If we look at the CDK 2.x version of those linked examples, we can see that they have a number of grant* functions for specific pre-defined 'sets' of permissions, rather than just the completely generic grant function we see on IUserPool currently:

It's been quite a while since I've worked on this code/with UserPool in general, so I'm not sure off the top of my head if/what 'pre-defined sets' of permissions might be useful from the 'total list', but if we look at my example above, the permissions used were:

cognito-idp:AdminGetUser
cognito-idp:AdminCreateUser
cognito-idp:AdminAddUserToGroup
cognito-idp:AdminEnableUser
cognito-idp:AdminDisableUser

And the high level goal was to, based on events sent from Stripe, be able to:

If I was to generalise that into a 'higher level category' that a grant* method could be made for, then I guess I would probably say something like 'UserPool user management' or similar (though for a category like that, there may be some additional permissions worth adding to it as well)