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

Cognito Construct: Add grant* methods #7112

Open
2 tasks
0xdevalias opened this issue Apr 1, 2020 · 15 comments
Open
2 tasks

Cognito Construct: Add grant* methods #7112

0xdevalias opened this issue Apr 1, 2020 · 15 comments
Labels
@aws-cdk/aws-cognito Related to Amazon Cognito effort/medium Medium work item – several days of effort feature-request A feature should be added or improved. p1

Comments

@0xdevalias
Copy link
Contributor

As per #6765 (comment), 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

Cognito

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

  • 👋 I may be able to implement this feature request
  • ⚠️ This feature might incur a breaking change

This is a 🚀 Feature Request

@0xdevalias 0xdevalias added feature-request A feature should be added or improved. needs-triage This issue or PR still needs to be triaged. labels Apr 1, 2020
@SomayaB SomayaB added the @aws-cdk/aws-cognito Related to Amazon Cognito label Apr 3, 2020
@nija-at
Copy link
Contributor

nija-at commented Apr 6, 2020

@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.

@nija-at nija-at added response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. and removed needs-triage This issue or PR still needs to be triaged. labels Apr 6, 2020
@0xdevalias
Copy link
Contributor Author

0xdevalias commented Apr 6, 2020

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
Copy link

pszabop commented Apr 13, 2020

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.

@SomayaB SomayaB removed the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Apr 23, 2020
@nija-at nija-at added the effort/medium Medium work item – several days of effort label May 12, 2020
@nija-at nija-at added the p1 label Aug 17, 2020
@aaronaustin
Copy link

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
Copy link
Contributor

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>

douglasnaphas added a commit to douglasnaphas/madliberation that referenced this issue Feb 19, 2021
gh-274

Taken from the workaround in the issue description here:
aws/aws-cdk#7112.

My error noted here:
aws/aws-cdk#7112 (comment)
@TomBonnerAtDerivitec
Copy link

TomBonnerAtDerivitec commented Jun 23, 2021

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
Copy link

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
Copy link

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
Copy link

derdeka commented Apr 13, 2023

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
Copy link
Contributor Author

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:

@pahud pahud added p2 and removed p1 labels Jun 11, 2024
@github-actions github-actions bot added p1 and removed p2 labels Jun 16, 2024
Copy link

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
Copy link
Contributor Author

0xdevalias commented Jun 16, 2024

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
Copy link
Contributor

mazyu36 commented Jun 16, 2024

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
Copy link
Contributor Author

0xdevalias commented Jun 16, 2024

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

class CustomNodeLambdaEventHandler extends Construct (wraps NodejsFunction)
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
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
Copy link
Contributor Author

0xdevalias commented Jun 16, 2024

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:

  • find/create users in the UserPool (eg. if they are a new Stripe customer, or if they've changed their subscription, etc)
  • add the users to an existing user group (eg. a group to denote they were created/managed from Stripe)
  • enable/disable the users (eg. disable them when their subscription ends, etc)

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-cognito Related to Amazon Cognito effort/medium Medium work item – several days of effort feature-request A feature should be added or improved. p1
Projects
None yet
Development

No branches or pull requests