Skip to content

Commit 662bea5

Browse files
fix(cli): address review feedback on storage access mapping
- Scope the test guard to the private/protected segments and assert no authenticated write/delete there. The previous negative regex stopped at the first ']' (inside the entity permission array) and never reached the authenticated call, so it could not catch a regression. - Make the pure render helpers (createAllowPattern, createEntityPattern, createResourcePattern) static. - Explain the intentional authenticated-read overlap on protected/ when the auth permission set is read-only. - Drop a redundant test comment.
1 parent 54d4c1c commit 662bea5

2 files changed

Lines changed: 29 additions & 14 deletions

File tree

packages/amplify-cli/src/__tests__/commands/gen2-migration/generate/amplify/storage/s3.generator.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,22 @@ describe('S3Generator', () => {
248248
const content = writtenFile('resource.ts');
249249
const normalized = content.replace(/\s+/g, ' ');
250250

251-
// Per-user paths use entity('identity') so {entity_id} maps to the caller's identity.
252251
expect(normalized).toMatch(
253252
/'private\/\{entity_id\}\/\*':\s*\[\s*allow\.entity\('identity'\)\.to\(\['read', 'write', 'delete'\]\)\s*,?\s*\]/,
254253
);
255254
expect(normalized).toMatch(/'protected\/\{entity_id\}\/\*':\s*\[\s*allow\.entity\('identity'\)\.to\(\['read', 'write', 'delete'\]\)/);
256255

257-
// Enforce per-user scoping: private/protected must not grant write/delete via allow.authenticated.
258-
expect(content).not.toMatch(/'(private|protected)\/\{entity_id\}\/\*': \[[^\]]*allow\.authenticated\.to\(\[[^\]]*'(write|delete)'/s);
256+
// private/ and protected/ must not grant write/delete via allow.authenticated (public/*
257+
// legitimately can, so the check is scoped to those paths). Rules render in the order
258+
// public, protected, private, so slice out each per-user segment and assert on it.
259+
const protectedSegment = normalized.slice(
260+
normalized.indexOf("'protected/{entity_id}/*'"),
261+
normalized.indexOf("'private/{entity_id}/*'"),
262+
);
263+
const privateSegment = normalized.slice(normalized.indexOf("'private/{entity_id}/*'"));
264+
for (const segment of [protectedSegment, privateSegment]) {
265+
expect(segment).not.toMatch(/allow\.authenticated\.to\(\[[^\]]*'(write|delete)'/);
266+
}
259267
});
260268

261269
it('renders guest access patterns', async () => {

packages/amplify-cli/src/commands/gen2-migration/generate/amplify/storage/s3.renderer.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,22 +277,25 @@ export class S3Renderer {
277277
const protectedPathAccess: CallExpression[] = [];
278278

279279
if (accessPatterns.guest && accessPatterns.guest.length > 0) {
280-
publicPathAccess.push(this.createAllowPattern(allowIdentifier, 'guest', accessPatterns.guest));
280+
publicPathAccess.push(S3Renderer.createAllowPattern(allowIdentifier, 'guest', accessPatterns.guest));
281281
}
282282
if (accessPatterns.auth && accessPatterns.auth.length > 0) {
283283
// public/* is a shared path: authenticated users get the Gen1 auth permission set.
284-
publicPathAccess.push(this.createAllowPattern(allowIdentifier, 'authenticated', accessPatterns.auth));
284+
publicPathAccess.push(S3Renderer.createAllowPattern(allowIdentifier, 'authenticated', accessPatterns.auth));
285285
// private/ and protected/ are per-user paths. Use allow.entity('identity') so the
286286
// {entity_id} token resolves to the caller's own Cognito identity, matching the
287287
// per-user scoping of the Gen1 configuration.
288-
privatePathAccess.push(this.createEntityPattern(allowIdentifier, accessPatterns.auth));
289-
protectedPathAccess.push(this.createEntityPattern(allowIdentifier, accessPatterns.auth));
290-
// Gen1 protected/ also grants read to other authenticated users.
291-
protectedPathAccess.push(this.createAllowPattern(allowIdentifier, 'authenticated', ['read']));
288+
privatePathAccess.push(S3Renderer.createEntityPattern(allowIdentifier, accessPatterns.auth));
289+
protectedPathAccess.push(S3Renderer.createEntityPattern(allowIdentifier, accessPatterns.auth));
290+
// Gen1 protected/ also grants read to other authenticated users. When the Gen1 auth
291+
// permission set is read-only, this authenticated read subsumes the owner's
292+
// entity('identity') read rule above; the overlap is harmless and keeps the per-path
293+
// mapping uniform across permission sets.
294+
protectedPathAccess.push(S3Renderer.createAllowPattern(allowIdentifier, 'authenticated', ['read']));
292295
}
293296
if (accessPatterns.groups) {
294297
for (const [groupName, permissions] of Object.entries(accessPatterns.groups)) {
295-
const pattern = this.createAllowPattern(allowIdentifier, `groups(['${groupName}'])`, permissions);
298+
const pattern = S3Renderer.createAllowPattern(allowIdentifier, `groups(['${groupName}'])`, permissions);
296299
publicPathAccess.push(pattern);
297300
privatePathAccess.push(pattern);
298301
protectedPathAccess.push(pattern);
@@ -308,7 +311,7 @@ export class S3Renderer {
308311
}
309312
}
310313
for (const [functionName, permissions] of Object.entries(consolidated)) {
311-
const pattern = this.createResourcePattern(allowIdentifier, functionName, Array.from(permissions));
314+
const pattern = S3Renderer.createResourcePattern(allowIdentifier, functionName, Array.from(permissions));
312315
publicPathAccess.push(pattern);
313316
privatePathAccess.push(pattern);
314317
protectedPathAccess.push(pattern);
@@ -349,7 +352,7 @@ export class S3Renderer {
349352
return factory.createPropertyAssignment(factory.createIdentifier('access'), accessFunction);
350353
}
351354

352-
private createAllowPattern(allowIdentifier: ts.Identifier, userLevel: string, permissions: readonly Permission[]): CallExpression {
355+
private static createAllowPattern(allowIdentifier: ts.Identifier, userLevel: string, permissions: readonly Permission[]): CallExpression {
353356
return factory.createCallExpression(
354357
factory.createPropertyAccessExpression(allowIdentifier, factory.createIdentifier(`${userLevel}.to`)),
355358
undefined,
@@ -361,7 +364,7 @@ export class S3Renderer {
361364
* Renders `allow.entity('identity').to([...])`, which scopes an {entity_id} path to the
362365
* caller's own Cognito identity in the generated IAM policy.
363366
*/
364-
private createEntityPattern(allowIdentifier: ts.Identifier, permissions: readonly Permission[]): CallExpression {
367+
private static createEntityPattern(allowIdentifier: ts.Identifier, permissions: readonly Permission[]): CallExpression {
365368
return factory.createCallExpression(
366369
factory.createPropertyAccessExpression(
367370
factory.createCallExpression(
@@ -376,7 +379,11 @@ export class S3Renderer {
376379
);
377380
}
378381

379-
private createResourcePattern(allowIdentifier: ts.Identifier, functionName: string, permissions: readonly Permission[]): CallExpression {
382+
private static createResourcePattern(
383+
allowIdentifier: ts.Identifier,
384+
functionName: string,
385+
permissions: readonly Permission[],
386+
): CallExpression {
380387
return factory.createCallExpression(
381388
factory.createPropertyAccessExpression(
382389
factory.createCallExpression(

0 commit comments

Comments
 (0)