amplify-category-api icon indicating copy to clipboard operation
amplify-category-api copied to clipboard

Lambda authorizer can not access DynamoDb table

Open hasan-aa opened this issue 1 year ago • 12 comments

Environment information

System:
    OS: macOS 13.2.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 14.41 GB / 64.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 20.10.0 - ~/.nvm/versions/node/v20.10.0/bin/node
    Yarn: 1.22.21 - ~/.nvm/versions/node/v20.10.0/bin/yarn
    npm: 10.2.3 - ~/.nvm/versions/node/v20.10.0/bin/npm
    pnpm: 8.14.0 - ~/.nvm/versions/node/v20.10.0/bin/pnpm
    bun: Not Found
    Watchman: Not Found
  npmPackages:
    @aws-amplify/backend: ^0.10.1 => 0.10.1 
    @aws-amplify/backend-cli: ^0.9.6 => 0.9.6 
    aws-amplify: ^6.0.12 => 6.0.12 
    aws-cdk: ^2.121.1 => 2.121.1 
    aws-cdk-lib: ^2.121.1 => 2.121.1 
    typescript: ^5.3.3 => 5.3.3

Description

I'm trying to add a lambda authorizer that reads data from dynamo db table to make authorization decision. But this lambda doesn't have proper IAM permissions to access the table.

I'm trying to customize the table resource to give access to the lambda function but there seems to be no way of accessing the proper table resource.

  • backend.data.resources.tables is always empty.
  • backend.data.resources.amplifyDynamoDbTables doesn't expose grantReadData function.

hasan-aa avatar Jan 24 '24 15:01 hasan-aa

Hey @hasan-aa :wave: thanks for raising this! I'm going to transfer this over to our API repo for tracking and better assistance 🙂

josefaidt avatar Jan 24 '24 16:01 josefaidt

I'm experiencing a similar issue but for GraphQL resolver functions. It might be a separate issue, though, because I think it would require also being able to reference the lambda function generated when using defineData and defineFunction together.

SalmonMode avatar Jan 24 '24 17:01 SalmonMode

@SalmonMode similarly I'm also not able to reference authorizer lambda function. This value is also always empty: backend.data.resources.cfnResources.cfnFunctions

hasan-aa avatar Jan 25 '24 08:01 hasan-aa

@hasan-aa I think I have something figured out. It looks like I can define a lambda function as I normally would, e.g. amplify/functions/someFunc/handler.ts and amplify/functions/someFunc/resource.ts, and include that in the call to defineBackend, e.g.:

amplify/functions/someFunc/resource.ts:

import { defineFunction } from '@aws-amplify/backend';

export const someFunc = defineFunction({
  /*
    name?: string // optional parameter to specify a function name. In this case, it will default to "someFunc" (the name of the directory where the function is defined)
    entry?: string // optional path to the function code. Defaults to ./handler.ts
  */
});

amplify/backend.ts:

import { someFunc } from "./functions/someFunc/resource";
export const backend = defineBackend({
  auth,
  data,
  someFunc,
});

Then in amplify/data/resources.ts, I can reference that function definition and set the authorization stuff like so:

import { someFunc } from "./functions/someFunc/resource";

const schema = a
  .schema({
    likeUser: a
      .mutation()
      .arguments({ profileId: a.string().required() })
      .returns(a.id())
      .authorization([a.allow.private("userPools")])
      .function("someFuncHandler"), // seems to determine the key looked for in the 'functions' section below
  })
  .authorization([a.allow.private("iam")]);


export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
  functions: {
    someFuncHandler: someFunc
  },
});

Then, in the main amplify/backend.ts file, I can define a standalone table, and give the function permission like so:

import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";
import { RemovalPolicy } from "aws-cdk-lib";
import { AttributeType, Table } from "aws-cdk-lib/aws-dynamodb";
import { someFunc} from "./functions/someFunc/resource";

export const backend = defineBackend({
  auth,
  data,
  someFunc,
});

const itemsTable = new Table(backend.someFunc.resources.lambda.stack, 'ItemsTable', {
  tableName: "things",
  partitionKey: {
    name: "id",
    type: AttributeType.STRING
  },
  removalPolicy: RemovalPolicy.DESTROY, // NOT recommended for production code
});
const lambda = backend.someFunc.resources.lambda;

itemsTable.grantFullAccess(lambda);

Maybe this can help you solve your issue.

SalmonMode avatar Jan 25 '24 14:01 SalmonMode

Thanks for the detailed explanation and code @SalmonMode! This looks very promising. I'll try that as soon as I can.

hasan-aa avatar Jan 25 '24 17:01 hasan-aa

Hey @SalmonMode thanks for the providing the workaround.

@hasan-aa Please refer to this doc page and let us know if it resolves your issue.

AnilMaktala avatar Jan 25 '24 19:01 AnilMaktala

Hello @AnilMaktala , What I've done already was based on the document you've provided. I've checked it once again but no luck.

I was able to go one step further following @SalmonMode suggestion like below though. But I still can not get a reference to the dynamoDB table resource. So I had to give access to all table resources for now.

import {defineBackend} from '@aws-amplify/backend';
import {auth} from './auth/resource';
import {data} from './data/resource';
import {customAuthorizer} from "./functions/resource";
import {Effect, PolicyStatement} from "aws-cdk-lib/aws-iam";

const backend = defineBackend({
    auth,
    data,
    customAuthorizer
});

let ddbReadPolicy = new PolicyStatement({
    effect: Effect.ALLOW,
    actions: [
        "dynamodb:BatchGetItem",
        "dynamodb:GetItem",
        "dynamodb:Scan",
        "dynamodb:Query",
        "dynamodb:GetRecords"],
// @ts-ignore
    resources: ['*'] // Table ARN is needed here.
})

backend.customAuthorizer.resources.lambda.addToRolePolicy(ddbReadPolicy)

hasan-aa avatar Jan 26 '24 08:01 hasan-aa

Are there any workarounds for this without needing to define the table directly in backend.ts? I am hoping to use the tables that are already defined in the data/resources.ts file. I as well strongly feel that giving Lambdas access to the table is a very basic requirement.

ideen1 avatar Feb 12 '24 10:02 ideen1

👋 - You can access the existing table info using this backend.data.resources.tables["Todo"].tableArn code snippet. Similarly, this should work too: backend.data.resources.tables["Todo"].grantReadData(...). The thing I'm not 100% sure yet is if it'll create a circular dependency issue.

renebrandel avatar Mar 28 '24 15:03 renebrandel

@renebrandel that does seem to cause a circular dependency. I am unable to give my lambda read access to my dynamodb table and also there doesn't seem to be a way give the lambda the generated name as an env variable

LukaASoban avatar May 08 '24 06:05 LukaASoban

If you are using the table name for queries/mutations on tables, the newer versions of Amplify Gen 2 allow you to access the dataClient server side very easily. If this is your goal you can check out this page which helped me: https://docs.amplify.aws/nextjs/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/

ideen1 avatar May 08 '24 07:05 ideen1

So I am using the generated table name like "UserProfile-xxjjshsidjt-dev" so that I can call dynamoDB functions directly within my lambda.

As far as I know there isn't a way to get that?

LukaASoban avatar May 08 '24 12:05 LukaASoban

I need my lambda function to access the table for my model too.

// Giving the lambda function access to the table like this works:
const permissionsStack = backend.createStack("PermissionsStack");
const createOrganizationFunctionLambda = backend.createOrganizationFunction.resources.lambda as lambda.Function;
new iam.Policy(permissionsStack, "CreateOrganizationOrganizationTablePolicy", {
  policyName: "CreateOrganizationOrganizationTablePolicy",
  roles: [createOrganizationFunctionLambda.role!],
  statements: [
    new iam.PolicyStatement({
      actions: ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:PutItem"],
      resources: [backend.data.resources.tables.Organization.tableArn],
    }),
  ],
});

// But passing the table name to the lambda function results in a circlar dependency error:
createOrganizationFunctionLambda.addEnvironment("ORGANIZATION_TABLE_NAME", backend.data.resources.tables.Organization.tableName ?? "");

It would be great if we could give our lambdas access to tables using a .access poperty on defineData or the schema and it creates the access policies and passes the table name as an environment variable to the lambda function.

Maybe similarly to the access property on storage:

access: (allow) => ({
    'MyTable*': [
      allow.resource(myLambdaFunction).to(['read', 'write', 'delete'])
    ]
  })

thomasoehri avatar May 27 '24 13:05 thomasoehri

@thomasoehri I had to revert to using the SSM Parameter store before we get more info on how to pass it without causing a circular dependency. It still works, but obviously would prefer to send the name directly.

import * as ssm from "aws-cdk-lib/aws-ssm";
import { Construct } from "constructs";

type MyParameterStoreProps = {
  parameters: { name: string; value: string }[];
};

export class MyParameterStore extends Construct {
  public readonly ssmStringParameters: ssm.StringParameter[];

  constructor(scope: Construct, id: string, props: MyParameterStoreProps) {
    super(scope, id);

    this.ssmStringParameters = props.parameters.map((param) => {
      return new ssm.StringParameter(this, param.name, {
        parameterName: param.name,
        stringValue: param.value,
      });
    });
  }
}

and then in your backend.ts

// Create parameters for MyParameterStore
const parameterStoreDynamoDBGeneratedKey = `/amplify/userProfileTableName-${process.env.AWS_BRANCH}`;

const myParameterStore = new myParameterStore(
  backend.createStack("myParameterStore"),
  "myParameterStore",
  {
    parameters: [
      {
        name: parameterStoreDynamoDBGeneratedKey,
        value: backend.data.resources.tables["UserProfile"].tableName,
      },
    ],
  }
);

.
.
.

const myLambda = backend.myFunction
  .resources.lambda as Function;

myLambda.addEnvironment(
  "SSM_USER_PROFILE_TABLE_NAME_KEY",
  parameterStoreDynamoDBGeneratedKey
);

and then in your lambda

// Get the table name from the SSM environment variable
  const ssmParameterName = env.SSM_USER_PROFILE_TABLE_NAME_KEY;
  const ssmInput: GetParameterCommandInput = {
    Name: ssmParameterName,
  };
  const ssmOutput = await ssmClient.send(new GetParameterCommand(ssmInput));

  const tableName = ssmOutput.Parameter?.Value;

Hope this helps!

LukaASoban avatar May 27 '24 13:05 LukaASoban

@LukaASoban Thank you so much for your workaround!

thomasoehri avatar May 28 '24 13:05 thomasoehri

any update on this one? I feel like this is a critical one for Amplify Gen 2 as a whole tbh. Not being able to pass in table names to resolvers without resorting to SSM param store or something else is pretty huge don't you think?

LukaASoban avatar Jul 01 '24 15:07 LukaASoban