amplify-category-api
amplify-category-api copied to clipboard
Lambda authorizer can not access DynamoDb table
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.
Hey @hasan-aa :wave: thanks for raising this! I'm going to transfer this over to our API repo for tracking and better assistance 🙂
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 similarly I'm also not able to reference authorizer lambda function.
This value is also always empty:
backend.data.resources.cfnResources.cfnFunctions
@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.
Thanks for the detailed explanation and code @SalmonMode! This looks very promising. I'll try that as soon as I can.
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.
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)
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.
👋 - 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 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
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/
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?
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 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 Thank you so much for your workaround!
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?