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

fix: fix policies with overridden table names #3075

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/amplify-category-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
"coverageProvider": "v8",
"coverageThreshold": {
"global": {
"branches": 68,
"branches": 67,
"functions": 42,
"lines": 40
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,50 @@ $util.toJson({})",
},
"Type": "AWS::IAM::Role",
},
"PostIAMRoleDefaultPolicy04190CA0": Object {
"Properties": Object {
"PolicyDocument": Object {
"Statement": Array [
Object {
"Action": Array [
"dynamodb:BatchGetItem",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:ConditionCheckItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
],
"Effect": "Allow",
"Resource": Array [
Object {
"Fn::GetAtt": Array [
"PostTable",
"Arn",
],
},
Object {
"Ref": "AWS::NoValue",
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "PostIAMRoleDefaultPolicy04190CA0",
"Roles": Array [
Object {
"Ref": "PostIAMRole83BF708F",
},
],
},
"Type": "AWS::IAM::Policy",
},
"PostTable": Object {
"DeletionPolicy": "Delete",
"Properties": Object {
Expand Down Expand Up @@ -1525,6 +1569,50 @@ Object {
},
"Type": "AWS::IAM::Role",
},
"CommentIAMRoleDefaultPolicyA8D6F6B5": Object {
"Properties": Object {
"PolicyDocument": Object {
"Statement": Array [
Object {
"Action": Array [
"dynamodb:BatchGetItem",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:ConditionCheckItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
],
"Effect": "Allow",
"Resource": Array [
Object {
"Fn::GetAtt": Array [
"CommentTable",
"Arn",
],
},
Object {
"Ref": "AWS::NoValue",
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "CommentIAMRoleDefaultPolicyA8D6F6B5",
"Roles": Array [
Object {
"Ref": "CommentIAMRoleD5EC5F51",
},
],
},
"Type": "AWS::IAM::Policy",
},
"CommentTable": Object {
"DeletionPolicy": "Delete",
"Properties": Object {
Expand Down Expand Up @@ -3006,6 +3094,50 @@ $util.toJson({})",
},
"Type": "AWS::IAM::Role",
},
"PostIAMRoleDefaultPolicy04190CA0": Object {
"Properties": Object {
"PolicyDocument": Object {
"Statement": Array [
Object {
"Action": Array [
"dynamodb:BatchGetItem",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:ConditionCheckItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
],
"Effect": "Allow",
"Resource": Array [
Object {
"Fn::GetAtt": Array [
"PostTable",
"Arn",
],
},
Object {
"Ref": "AWS::NoValue",
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "PostIAMRoleDefaultPolicy04190CA0",
"Roles": Array [
Object {
"Ref": "PostIAMRole83BF708F",
},
],
},
"Type": "AWS::IAM::Policy",
},
"PostTable": Object {
"DeletionPolicy": "Delete",
"Properties": Object {
Expand Down Expand Up @@ -3748,6 +3880,50 @@ Object {
},
"Type": "AWS::IAM::Role",
},
"CommentIAMRoleDefaultPolicyA8D6F6B5": Object {
"Properties": Object {
"PolicyDocument": Object {
"Statement": Array [
Object {
"Action": Array [
"dynamodb:BatchGetItem",
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:ConditionCheckItem",
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:DescribeTable",
],
"Effect": "Allow",
"Resource": Array [
Object {
"Fn::GetAtt": Array [
"CommentTable",
"Arn",
],
},
Object {
"Ref": "AWS::NoValue",
},
],
},
],
"Version": "2012-10-17",
},
"PolicyName": "CommentIAMRoleDefaultPolicyA8D6F6B5",
"Roles": Array [
Object {
"Ref": "CommentIAMRoleD5EC5F51",
},
],
},
"Type": "AWS::IAM::Policy",
},
"CommentTable": Object {
"DeletionPolicy": "Delete",
"Properties": Object {
Expand Down
30 changes: 29 additions & 1 deletion packages/amplify-e2e-core/src/utils/sdk-calls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,35 @@ export const getAmplifyBackendJobStatus = async (jobId: string, appId: string, e
.promise();
};

export const listRolePolicies = async (roleName: string, region: string) => {
export const listRoleNamesContaining = async (searchString: string, region: string): Promise<string[]> => {
const service = new IAM({ region });

const roles: string[] = [];
let isTruncated = true;
let marker: string | undefined;

while (isTruncated) {
const params = marker ? { Marker: marker } : {};
const response = await service.listRoles(params).promise();
Copy link
Contributor

Choose a reason for hiding this comment

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

does this need paginated?

Copy link
Member Author

@palpatim palpatim Jan 6, 2025

Choose a reason for hiding this comment

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

Late response to this comment: the listRoles response is paginated, using Marker as the "next token" equivalent. See https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/iam/command/ListRolesCommand/


const matchingRoles = response.Roles.filter((role) => role.RoleName.includes(searchString));
roles.push(...matchingRoles.map((r) => r.RoleName));

isTruncated = response.IsTruncated;
marker = response.Marker;
}

return roles;
};

export const getRolePolicy = async (roleName: string, policyName: string, region: string): Promise<any> => {
const service = new IAM({ region });
const rawDocument = (await service.getRolePolicy({ PolicyName: policyName, RoleName: roleName }).promise()).PolicyDocument;
const decodedDocument = decodeURIComponent(rawDocument);
return JSON.parse(decodedDocument);
};

export const listRolePolicies = async (roleName: string, region: string): Promise<string[]> => {
const service = new IAM({ region });
return (await service.listRolePolicies({ RoleName: roleName }).promise()).PolicyNames;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import path from 'path';
import * as fs from 'fs-extra';
import {
addApiWithoutSchema,
amplifyOverrideApi,
amplifyPush,
amplifyPushOverride,
createNewProjectDir,
deleteProject,
deleteProjectDir,
getAppSyncApi,
getDDBTable,
getProjectMeta,
getRolePolicy,
initJSProjectWithProfile,
listRolePolicies,
listRoleNamesContaining,
replaceOverrideFileWithProjectInfo,
updateApiSchema,
updateSchema,
} from 'amplify-category-api-e2e-core';

describe('Override table name', () => {
let projRoot: string;
let projFolderName: string;
beforeEach(async () => {
projFolderName = 'overridename';
projRoot = await createNewProjectDir(projFolderName);
});

afterEach(async () => {
const metaFilePath = path.join(projRoot, 'amplify', '#current-cloud-backend', 'amplify-meta.json');
if (fs.existsSync(metaFilePath)) {
await deleteProject(projRoot);
}
deleteProjectDir(projRoot);
});

it('Generates correct permissions policies for DynamoDB tables with overridden names', async () => {
const now = Math.floor(Date.now() / 1000);
const modelName = `Override${now}`;

const schema = /* GraphQL */ `
type ${modelName} @model {
id: ID!
content: String
}
`;

const envName = 'integtest';
const projName = 'overridetest';
const cliInputsFilePath = path.join(projRoot, 'amplify', 'backend', 'api', `${projName}`, 'cli-inputs.json');
await initJSProjectWithProfile(projRoot, { name: projName, envName });
await addApiWithoutSchema(projRoot);

updateSchema(projRoot, projName, schema);
expect(fs.existsSync(cliInputsFilePath)).toBe(true);

await amplifyPush(projRoot);

await amplifyOverrideApi(projRoot, {});

const overriddenTableName = `OverrideTest${now}Custom`;
const overrideCode = /* TypeScript */ `
export function override(props: any) {
props.models['${modelName}'].modelDDBTable.tableName = '${overriddenTableName}';
}
`;
const destOverrideFilePath = path.join(projRoot, 'amplify', 'backend', 'api', `${projName}`, 'override.ts');
fs.writeFileSync(destOverrideFilePath, overrideCode);

await amplifyPushOverride(projRoot);

const meta = getProjectMeta(projRoot);
const region = meta.providers.awscloudformation.Region;
const { output } = meta.api.overridetest;
const { GraphQLAPIIdOutput, GraphQLAPIEndpointOutput, GraphQLAPIKeyOutput } = output;
const { graphqlApi } = await getAppSyncApi(GraphQLAPIIdOutput, region);

expect(graphqlApi).toBeDefined();
expect(graphqlApi.apiId).toEqual(GraphQLAPIIdOutput);
expect(GraphQLAPIIdOutput).toBeDefined();
expect(GraphQLAPIEndpointOutput).toBeDefined();
expect(GraphQLAPIKeyOutput).toBeDefined();

const defaultTableName = `${modelName}-${graphqlApi.apiId}-${envName}`;
const error = { message: null };
try {
const defaultTable = await getDDBTable(defaultTableName, region);
expect(defaultTable).toBeUndefined();
} catch (ex) {
Object.assign(error, ex);
}
expect(error).toBeDefined();
expect(error.message).toContain(`${defaultTableName} not found`);

const actualTable = await getDDBTable(overriddenTableName, region);
expect(actualTable).toBeDefined();

// Validate policy. The role will be created with the prefix {modelName}IAMRole. It should have 2 policies: one created by Amplify, one
// created by AppSync's CDK call during the `addDynamoDbDataSource` flow. We expect the policy statements for the latter to refer to the
// overridden table name.
const matchingRoleNames = await listRoleNamesContaining(modelName, region);
expect(matchingRoleNames).toBeDefined();
expect(matchingRoleNames.length).toEqual(1);
const roleName = matchingRoleNames[0];

const policies = await listRolePolicies(roleName, region);
expect(policies).toBeDefined();
expect(policies.length).toBe(2);

const defaultPolicy = policies.find((p) => p.startsWith(`${modelName}IAMRoleDefault`));
expect(defaultPolicy).toBeDefined();

const policyObject = await getRolePolicy(roleName, defaultPolicy, region);
expect(policyObject).toBeDefined();
expect(policyObject.Statement[0].Resource[0]).toContain(overriddenTableName);
});
});
2 changes: 1 addition & 1 deletion packages/amplify-graphql-auth-transformer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"coverageProvider": "v8",
"coverageThreshold": {
"global": {
"branches": 88,
"branches": 87,
"functions": 90,
"lines": 90
}
Expand Down
Loading
Loading