Skip to content

Commit 283edd6

Browse files
authored
fix(redshift-alpha): extract tableName from custom resource functions (#32452)
### Issue # (if applicable) Closes - #31817 - #29231 ### Reason for this change This regression was introduced in PR#24308, which added support for executing the `ALT TABLE` SQL statement when the table name is changed in the CDK construct. The root cause is in the modification of the physical ID of the custom resource Lambda function. Previously, this Lambda function was returning the `tableName` after table is created. However, the physical ID was updated to a combined ID in the format `clusterId:databaseName:tableNamePrefix:stackIdSuffix`. This change breaks the logic that depends on the return value being used as the `tableName`. For example, in the reported issue, the integration test relies on this value to grant permissions to specific users. The new combined ID format causes SQL statement errors, as described in the issue. ### Description of changes The fix involves adding logic to detect whether the resource's physical ID is in the new format. If it is, the logic reconstructs the `tableName` from the combined ID. This ensures that Lambda functions referencing the `tableName` receive the correct value. ### Description of how you validated changes - rerun unit test cases - deployed integration test cases ### Checklist - [X] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 934c9dc commit 283edd6

File tree

144 files changed

+4136
-3421
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+4136
-3421
lines changed

packages/@aws-cdk/aws-redshift-alpha/lib/private/database-query-provider/privileges.ts

+41-10
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ export async function handler(props: UserTablePrivilegesHandlerProps & ClusterPr
1111
const clusterProps = props;
1212

1313
if (event.RequestType === 'Create') {
14-
await grantPrivileges(username, tablePrivileges, clusterProps);
14+
await grantPrivileges(username, tablePrivileges, clusterProps, event.StackId);
1515
return { PhysicalResourceId: makePhysicalId(username, clusterProps, event.RequestId) };
1616
} else if (event.RequestType === 'Delete') {
17-
await revokePrivileges(username, tablePrivileges, clusterProps);
17+
await revokePrivileges(username, tablePrivileges, clusterProps, event.StackId);
1818
return;
1919
} else if (event.RequestType === 'Update') {
2020
const { replace } = await updatePrivileges(
2121
username,
2222
tablePrivileges,
2323
clusterProps,
2424
event.OldResourceProperties as unknown as UserTablePrivilegesHandlerProps & ClusterProps,
25+
event.StackId,
2526
);
2627
const physicalId = replace ? makePhysicalId(username, clusterProps, event.RequestId) : event.PhysicalResourceId;
2728
return { PhysicalResourceId: physicalId };
@@ -31,19 +32,35 @@ export async function handler(props: UserTablePrivilegesHandlerProps & ClusterPr
3132
}
3233
}
3334

34-
async function revokePrivileges(username: string, tablePrivileges: TablePrivilege[], clusterProps: ClusterProps) {
35+
async function revokePrivileges(
36+
username: string,
37+
tablePrivileges: TablePrivilege[],
38+
clusterProps: ClusterProps,
39+
stackId: string,
40+
) {
3541
// Limited by human input
3642
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
3743
await Promise.all(tablePrivileges.map(({ tableName, actions }) => {
38-
return executeStatement(`REVOKE ${actions.join(', ')} ON ${tableName} FROM ${username}`, clusterProps);
44+
return executeStatement(
45+
`REVOKE ${actions.join(', ')} ON ${normalizedTableName(tableName, stackId)} FROM ${username}`,
46+
clusterProps,
47+
);
3948
}));
4049
}
4150

42-
async function grantPrivileges(username: string, tablePrivileges: TablePrivilege[], clusterProps: ClusterProps) {
51+
async function grantPrivileges(
52+
username: string,
53+
tablePrivileges: TablePrivilege[],
54+
clusterProps: ClusterProps,
55+
stackId: string,
56+
) {
4357
// Limited by human input
4458
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
4559
await Promise.all(tablePrivileges.map(({ tableName, actions }) => {
46-
return executeStatement(`GRANT ${actions.join(', ')} ON ${tableName} TO ${username}`, clusterProps);
60+
return executeStatement(
61+
`GRANT ${actions.join(', ')} ON ${normalizedTableName(tableName, stackId)} TO ${username}`,
62+
clusterProps,
63+
);
4764
}));
4865
}
4966

@@ -52,16 +69,17 @@ async function updatePrivileges(
5269
tablePrivileges: TablePrivilege[],
5370
clusterProps: ClusterProps,
5471
oldResourceProperties: UserTablePrivilegesHandlerProps & ClusterProps,
72+
stackId: string,
5573
): Promise<{ replace: boolean }> {
5674
const oldClusterProps = oldResourceProperties;
5775
if (clusterProps.clusterName !== oldClusterProps.clusterName || clusterProps.databaseName !== oldClusterProps.databaseName) {
58-
await grantPrivileges(username, tablePrivileges, clusterProps);
76+
await grantPrivileges(username, tablePrivileges, clusterProps, stackId);
5977
return { replace: true };
6078
}
6179

6280
const oldUsername = oldResourceProperties.username;
6381
if (oldUsername !== username) {
64-
await grantPrivileges(username, tablePrivileges, clusterProps);
82+
await grantPrivileges(username, tablePrivileges, clusterProps, stackId);
6583
return { replace: true };
6684
}
6785

@@ -72,7 +90,7 @@ async function updatePrivileges(
7290
))
7391
));
7492
if (tablesToRevoke.length > 0) {
75-
await revokePrivileges(username, tablesToRevoke, clusterProps);
93+
await revokePrivileges(username, tablesToRevoke, clusterProps, stackId);
7694
}
7795

7896
const tablesToGrant = tablePrivileges.filter(({ tableId, tableName, actions }) => {
@@ -85,8 +103,21 @@ async function updatePrivileges(
85103
return tableAdded || actionsAdded;
86104
});
87105
if (tablesToGrant.length > 0) {
88-
await grantPrivileges(username, tablesToGrant, clusterProps);
106+
await grantPrivileges(username, tablesToGrant, clusterProps, stackId);
89107
}
90108

91109
return { replace: false };
92110
}
111+
112+
/**
113+
* We need this normalization logic because some of the `TableName` values
114+
* are physical IDs generated in the {@link makePhysicalId} function.
115+
* */
116+
const normalizedTableName = (tableName: string, stackId: string): string => {
117+
const segments = tableName.split(':');
118+
const suffix = segments.slice(-1);
119+
if (suffix != null && stackId.endsWith(suffix[0])) {
120+
return segments.slice(-2)[0] ?? tableName;
121+
}
122+
return tableName;
123+
};

packages/@aws-cdk/aws-redshift-alpha/lib/private/database-query-provider/table.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export async function handler(props: TableAndClusterProps, event: AWSLambda.Clou
1515

1616
if (event.RequestType === 'Create') {
1717
tableName = await createTable(tableNamePrefix, getTableNameSuffix(props.tableName.generateSuffix), tableColumns, tableAndClusterProps);
18-
return { PhysicalResourceId: makePhysicalId(tableNamePrefix, tableAndClusterProps, event.StackId.substring(event.StackId.length - 12)) };
18+
return { PhysicalResourceId: makePhysicalId(tableName, tableAndClusterProps, event.StackId.substring(event.StackId.length - 12)) };
1919
} else if (event.RequestType === 'Delete') {
2020
await dropTable(
2121
event.PhysicalResourceId.includes(event.StackId.substring(event.StackId.length - 12)) ? tableName : event.PhysicalResourceId,

packages/@aws-cdk/aws-redshift-alpha/lib/private/privileges.ts

+35-30
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ITable, TableAction } from '../table';
55
import { IUser } from '../user';
66
import { DatabaseQuery } from './database-query';
77
import { HandlerName } from './database-query-provider/handler-name';
8-
import { TablePrivilege as SerializedTablePrivilege, UserTablePrivilegesHandlerProps } from './handler-props';
8+
import { UserTablePrivilegesHandlerProps } from './handler-props';
99

1010
/**
1111
* The Redshift table and action that make up a privilege that can be granted to a Redshift user.
@@ -61,35 +61,14 @@ export class UserTablePrivileges extends Construct {
6161
properties: {
6262
username: props.user.username,
6363
tablePrivileges: cdk.Lazy.any({
64-
produce: () => {
65-
const reducedPrivileges = this.privileges.reduce((privileges, { table, actions }) => {
66-
const tableId = table.node.id;
67-
if (!(tableId in privileges)) {
68-
privileges[tableId] = {
69-
tableName: table.tableName,
70-
actions: [],
71-
};
72-
}
73-
actions = actions.concat(privileges[tableId].actions);
74-
if (actions.includes(TableAction.ALL)) {
75-
actions = [TableAction.ALL];
76-
}
77-
if (actions.includes(TableAction.UPDATE) || actions.includes(TableAction.DELETE)) {
78-
actions.push(TableAction.SELECT);
79-
}
80-
privileges[tableId] = {
81-
tableName: table.tableName,
82-
actions: Array.from(new Set(actions)),
83-
};
84-
return privileges;
85-
}, {} as { [key: string]: { tableName: string; actions: TableAction[] } });
86-
const serializedPrivileges: SerializedTablePrivilege[] = Object.entries(reducedPrivileges).map(([tableId, config]) => ({
87-
tableId,
88-
tableName: config.tableName,
89-
actions: config.actions.map(action => TableAction[action]),
90-
}));
91-
return serializedPrivileges;
92-
},
64+
produce: () =>
65+
Object.entries(groupPrivilegesByTable(this.privileges))
66+
.map(([tableId, tablePrivileges]) => ({
67+
tableId,
68+
// The first element always exists since the groupBy element is at least a singleton.
69+
tableName: tablePrivileges[0]!.table.tableName,
70+
actions: unifyTableActions(tablePrivileges).map(action => TableAction[action]),
71+
})),
9372
}) as any,
9473
},
9574
});
@@ -102,3 +81,29 @@ export class UserTablePrivileges extends Construct {
10281
this.privileges.push({ table, actions });
10382
}
10483
}
84+
85+
const unifyTableActions = (tablePrivileges: TablePrivilege[]): TableAction[] => {
86+
const set = new Set<TableAction>(tablePrivileges.flatMap(x => x.actions));
87+
88+
if (set.has(TableAction.ALL)) {
89+
return [TableAction.ALL];
90+
}
91+
92+
if (set.has(TableAction.UPDATE) || set.has(TableAction.DELETE)) {
93+
set.add(TableAction.SELECT);
94+
}
95+
96+
return [...set];
97+
};
98+
99+
const groupPrivilegesByTable = (privileges: TablePrivilege[]): Record<string, TablePrivilege[]> => {
100+
return privileges.reduce((grouped, privilege) => {
101+
const { table } = privilege;
102+
const tableId = table.node.id;
103+
const tablePrivileges = grouped[tableId] ?? [];
104+
return {
105+
...grouped,
106+
[tableId]: [...tablePrivileges, privilege],
107+
};
108+
}, {} as Record<string, TablePrivilege[]>);
109+
};

packages/@aws-cdk/aws-redshift-alpha/lib/table.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export class Table extends TableBase {
272272
properties: {
273273
tableName: {
274274
prefix: props.tableName ?? cdk.Names.uniqueId(this),
275-
generateSuffix: !props.tableName ? 'true' : 'false',
275+
generateSuffix: (props.tableName == null).toString(),
276276
},
277277
tableColumns: this.tableColumns,
278278
distStyle: props.distStyle,
@@ -282,7 +282,7 @@ export class Table extends TableBase {
282282
},
283283
});
284284

285-
this.tableName = this.resource.ref;
285+
this.tableName = props.tableName ?? this.resource.ref;
286286
}
287287

288288
/**

packages/@aws-cdk/aws-redshift-alpha/test/database-query-provider/privileges.test.ts

+95
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jest.mock('@aws-sdk/client-redshift-data', () => {
4040
});
4141

4242
import { handler as managePrivileges } from '../../lib/private/database-query-provider/privileges';
43+
import { makePhysicalId } from '../../lib/private/database-query-provider/util';
4344

4445
beforeEach(() => {
4546
jest.clearAllMocks();
@@ -61,6 +62,31 @@ describe('create', () => {
6162
Sql: `GRANT INSERT, SELECT ON ${tableName} TO ${username}`,
6263
}));
6364
});
65+
66+
test('serializes properties in statement when tableName in physical resource ID', async () => {
67+
const properties = {
68+
...resourceProperties,
69+
tablePrivileges: [{
70+
tableId,
71+
tableName: `${makePhysicalId(tableName, resourceProperties, requestId)}`,
72+
actions,
73+
}],
74+
};
75+
76+
const event = {
77+
...baseEvent,
78+
ResourceProperties: properties,
79+
StackId: 'xxxxx:' + requestId,
80+
};
81+
82+
await expect(managePrivileges(properties, event)).resolves.toEqual({
83+
PhysicalResourceId: 'clusterName:databaseName:username:requestId',
84+
});
85+
86+
expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({
87+
Sql: `GRANT INSERT, SELECT ON ${tableName} TO ${username}`,
88+
}));
89+
});
6490
});
6591

6692
describe('delete', () => {
@@ -79,6 +105,29 @@ describe('delete', () => {
79105
Sql: `REVOKE INSERT, SELECT ON ${tableName} FROM ${username}`,
80106
}));
81107
});
108+
109+
test('serializes properties in statement when tableName in physical resource ID', async () => {
110+
const properties = {
111+
...resourceProperties,
112+
tablePrivileges: [{
113+
tableId,
114+
tableName: `${makePhysicalId(tableName, resourceProperties, requestId)}`,
115+
actions,
116+
}],
117+
};
118+
119+
const event = {
120+
...baseEvent,
121+
ResourceProperties: properties,
122+
StackId: 'xxxxx:' + requestId,
123+
};
124+
125+
await managePrivileges(properties, event);
126+
127+
expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({
128+
Sql: `REVOKE INSERT, SELECT ON ${tableName} FROM ${username}`,
129+
}));
130+
});
82131
});
83132

84133
describe('update', () => {
@@ -244,4 +293,50 @@ describe('update', () => {
244293
Sql: expect.stringMatching(new RegExp(`.+ ON ${tableName} FROM ${username}`)),
245294
}));
246295
});
296+
297+
test('serializes properties in grant statement when tableName in physical resource ID', async () => {
298+
const properties = {
299+
...resourceProperties,
300+
tablePrivileges: [{
301+
tableId,
302+
tableName: `${makePhysicalId(tableName, resourceProperties, requestId)}`,
303+
actions,
304+
}],
305+
};
306+
307+
const newEvent = {
308+
...event,
309+
ResourceProperties: properties,
310+
StackId: 'xxxxx:' + requestId,
311+
};
312+
313+
await managePrivileges(properties, newEvent);
314+
315+
expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({
316+
Sql: `GRANT INSERT, SELECT ON ${tableName} TO ${username}`,
317+
}));
318+
});
319+
320+
test('serializes properties in drop statement when tableName in physical resource ID', async () => {
321+
const properties = {
322+
...resourceProperties,
323+
tablePrivileges: [{
324+
tableId,
325+
tableName: `${makePhysicalId(tableName, resourceProperties, requestId)}`,
326+
actions: ['DROP'],
327+
}],
328+
};
329+
330+
const newEvent = {
331+
...event,
332+
ResourceProperties: properties,
333+
StackId: 'xxxxx:' + requestId,
334+
};
335+
336+
await managePrivileges(properties, newEvent);
337+
338+
expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({
339+
Sql: `REVOKE INSERT, SELECT ON ${tableName} FROM ${username}`,
340+
}));
341+
});
247342
});

packages/@aws-cdk/aws-redshift-alpha/test/database-query-provider/table.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('create', () => {
6161
const event = baseEvent;
6262

6363
await expect(manageTable(resourceProperties, event)).resolves.toEqual({
64-
PhysicalResourceId: 'clusterName:databaseName:tableNamePrefix:111111111111',
64+
PhysicalResourceId: 'clusterName:databaseName:tableNamePrefix111111111111:111111111111',
6565
});
6666
expect(mockExecuteStatement).toHaveBeenCalledWith(expect.objectContaining({
6767
Sql: `CREATE TABLE ${tableNamePrefix}${stackIdTruncated} (col1 varchar(1))`,

packages/@aws-cdk/aws-redshift-alpha/test/integ.cluster-az-relocation.js.snapshot/manifest.json

-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)