Skip to content

Commit 82006e5

Browse files
authored
Add "list" action to storage resource access (#1099)
1 parent 5d3e5bd commit 82006e5

File tree

8 files changed

+234
-44
lines changed

8 files changed

+234
-44
lines changed

.changeset/short-olives-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/backend-storage': minor
3+
---
4+
5+
Add "list" to available storage resource actions

packages/backend-storage/API.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ export type StorageAccessGenerator = (allow: StorageAccessBuilder) => StorageAcc
5454
// @public (undocumented)
5555
export type StorageAccessRecord = Record<StoragePath, StorageAccessDefinition[]>;
5656

57-
// @public (undocumented)
58-
export type StorageAction = 'read' | 'write' | 'delete';
57+
// @public
58+
export type StorageAction = 'read' | 'get' | 'list' | 'write' | 'delete';
5959

6060
// @public (undocumented)
6161
export type StorageActionBuilder = {

packages/backend-storage/src/private_types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
* Types that should remain internal to the package
33
*/
44

5+
import { StorageAction } from './types.js';
6+
57
/**
68
* Storage user error types
79
*/
810
export type StorageError = 'InvalidStorageAccessPathError';
11+
12+
/**
13+
* StorageAction type intended to be used after mapping "read" to "get" and "list"
14+
*/
15+
export type InternalStorageAction = Exclude<StorageAction, 'read'>;

packages/backend-storage/src/storage_access_orchestrator.test.ts

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ void describe('StorageAccessOrchestrator', () => {
3030
() => ({
3131
'/test/prefix/*': [
3232
{
33-
actions: ['read', 'write'],
33+
actions: ['get', 'write'],
3434
getResourceAccessAcceptor: () => ({
3535
identifier: 'testResourceAccessAcceptor',
3636
acceptResourceAccess: acceptResourceAccessMock,
@@ -59,7 +59,7 @@ void describe('StorageAccessOrchestrator', () => {
5959
() => ({
6060
'/test/prefix/*': [
6161
{
62-
actions: ['read', 'write'],
62+
actions: ['get', 'write'],
6363
getResourceAccessAcceptor: () => ({
6464
identifier: 'testResourceAccessAcceptor',
6565
acceptResourceAccess: acceptResourceAccessMock,
@@ -109,14 +109,14 @@ void describe('StorageAccessOrchestrator', () => {
109109
() => ({
110110
'/test/prefix/*': [
111111
{
112-
actions: ['read', 'write', 'delete'],
112+
actions: ['get', 'write', 'delete'],
113113
getResourceAccessAcceptor: getResourceAccessAcceptorStub,
114114
ownerPlaceholderSubstitution: '*',
115115
},
116116
],
117117
'/another/prefix/*': [
118118
{
119-
actions: ['read'],
119+
actions: ['get'],
120120
getResourceAccessAcceptor: getResourceAccessAcceptorStub,
121121
ownerPlaceholderSubstitution: '*',
122122
},
@@ -176,19 +176,19 @@ void describe('StorageAccessOrchestrator', () => {
176176
() => ({
177177
'/test/prefix/*': [
178178
{
179-
actions: ['read', 'write', 'delete'],
179+
actions: ['get', 'write', 'delete'],
180180
getResourceAccessAcceptor: getResourceAccessAcceptorStub1,
181181
ownerPlaceholderSubstitution: '*',
182182
},
183183
{
184-
actions: ['read'],
184+
actions: ['get'],
185185
getResourceAccessAcceptor: getResourceAccessAcceptorStub2,
186186
ownerPlaceholderSubstitution: '*',
187187
},
188188
],
189189
'/another/prefix/*': [
190190
{
191-
actions: ['read', 'delete'],
191+
actions: ['get', 'delete'],
192192
getResourceAccessAcceptor: getResourceAccessAcceptorStub2,
193193
ownerPlaceholderSubstitution: '*',
194194
},
@@ -262,7 +262,7 @@ void describe('StorageAccessOrchestrator', () => {
262262
() => ({
263263
[`/test/${ownerPathPartToken}/*`]: [
264264
{
265-
actions: ['read', 'write'],
265+
actions: ['get', 'write'],
266266
getResourceAccessAcceptor: () => ({
267267
identifier: 'testResourceAccessAcceptor',
268268
acceptResourceAccess: acceptResourceAccessMock,
@@ -309,7 +309,7 @@ void describe('StorageAccessOrchestrator', () => {
309309
() => ({
310310
'/foo/*': [
311311
{
312-
actions: ['read', 'write'],
312+
actions: ['get', 'write'],
313313
getResourceAccessAcceptor: () => ({
314314
identifier: 'resourceAccessAcceptor1',
315315
acceptResourceAccess: acceptResourceAccessMock1,
@@ -319,7 +319,7 @@ void describe('StorageAccessOrchestrator', () => {
319319
],
320320
'/foo/bar/*': [
321321
{
322-
actions: ['read'],
322+
actions: ['get'],
323323
getResourceAccessAcceptor: () => ({
324324
identifier: 'resourceAccessAcceptor2',
325325
acceptResourceAccess: acceptResourceAccessMock2,
@@ -400,7 +400,7 @@ void describe('StorageAccessOrchestrator', () => {
400400
ownerPlaceholderSubstitution: '{ownerSub}',
401401
},
402402
{
403-
actions: ['read'],
403+
actions: ['get'],
404404
getResourceAccessAcceptor: authenticatedResourceAccessAcceptor,
405405
ownerPlaceholderSubstitution: '*',
406406
},
@@ -460,7 +460,7 @@ void describe('StorageAccessOrchestrator', () => {
460460
// acceptor2 should not have any rules for this path
461461
'/foo/*': [
462462
{
463-
actions: ['read', 'write'],
463+
actions: ['get', 'write'],
464464
getResourceAccessAcceptor: getResourceAccessAcceptorStub1,
465465
ownerPlaceholderSubstitution: '*',
466466
},
@@ -469,7 +469,7 @@ void describe('StorageAccessOrchestrator', () => {
469469
// acceptor2 should have only read on this path
470470
'/foo/bar/*': [
471471
{
472-
actions: ['read'],
472+
actions: ['get'],
473473
getResourceAccessAcceptor: getResourceAccessAcceptorStub2,
474474
ownerPlaceholderSubstitution: '{ownerSub}',
475475
},
@@ -478,7 +478,7 @@ void describe('StorageAccessOrchestrator', () => {
478478
// acceptor2 should not have any rules for this path
479479
'/foo/baz/*': [
480480
{
481-
actions: ['read'],
481+
actions: ['get'],
482482
ownerPlaceholderSubstitution: '*',
483483
getResourceAccessAcceptor: getResourceAccessAcceptorStub1,
484484
},
@@ -487,12 +487,12 @@ void describe('StorageAccessOrchestrator', () => {
487487
// acceptor 2 has read/write/delete on path with ownerSub
488488
'/other/{owner}/*': [
489489
{
490-
actions: ['read', 'write', 'delete'],
490+
actions: ['get', 'write', 'delete'],
491491
getResourceAccessAcceptor: getResourceAccessAcceptorStub2,
492492
ownerPlaceholderSubstitution: '{ownerSub}',
493493
},
494494
{
495-
actions: ['read'],
495+
actions: ['get'],
496496
getResourceAccessAcceptor: getResourceAccessAcceptorStub1,
497497
ownerPlaceholderSubstitution: '*',
498498
},
@@ -584,7 +584,7 @@ void describe('StorageAccessOrchestrator', () => {
584584
() => ({
585585
'/foo/*': [
586586
{
587-
actions: ['read'],
587+
actions: ['get'],
588588
getResourceAccessAcceptor: authenticatedResourceAccessAcceptor,
589589
ownerPlaceholderSubstitution: '*',
590590
},
@@ -635,6 +635,79 @@ void describe('StorageAccessOrchestrator', () => {
635635
ssmEnvironmentEntriesStub
636636
);
637637
});
638+
639+
void it('replaces "read" access with "get" and "list" and merges duplicate actions', () => {
640+
const acceptResourceAccessMock = mock.fn();
641+
const authenticatedResourceAccessAcceptor = () => ({
642+
identifier: 'authenticatedResourceAccessAcceptor',
643+
acceptResourceAccess: acceptResourceAccessMock,
644+
});
645+
646+
const storageAccessOrchestrator = new StorageAccessOrchestrator(
647+
() => ({
648+
'/foo/bar/*': [
649+
{
650+
actions: ['read', 'get', 'list'],
651+
getResourceAccessAcceptor: authenticatedResourceAccessAcceptor,
652+
ownerPlaceholderSubstitution: '*',
653+
},
654+
{
655+
actions: ['list'],
656+
getResourceAccessAcceptor: authenticatedResourceAccessAcceptor,
657+
ownerPlaceholderSubstitution: '*',
658+
},
659+
],
660+
'/other/baz/*': [
661+
{
662+
actions: ['read'],
663+
getResourceAccessAcceptor: authenticatedResourceAccessAcceptor,
664+
ownerPlaceholderSubstitution: '*',
665+
},
666+
],
667+
}),
668+
{} as unknown as ConstructFactoryGetInstanceProps,
669+
ssmEnvironmentEntriesStub,
670+
storageAccessPolicyFactory
671+
);
672+
673+
storageAccessOrchestrator.orchestrateStorageAccess();
674+
assert.equal(acceptResourceAccessMock.mock.callCount(), 1);
675+
assert.deepStrictEqual(
676+
acceptResourceAccessMock.mock.calls[0].arguments[0].document.toJSON(),
677+
{
678+
Statement: [
679+
{
680+
Action: 's3:GetObject',
681+
Effect: 'Allow',
682+
Resource: [
683+
`${bucket.bucketArn}/foo/bar/*`,
684+
`${bucket.bucketArn}/other/baz/*`,
685+
],
686+
},
687+
{
688+
Action: 's3:ListBucket',
689+
Effect: 'Allow',
690+
Resource: bucket.bucketArn,
691+
Condition: {
692+
StringLike: {
693+
's3:prefix': [
694+
'foo/bar/*',
695+
'foo/bar/',
696+
'other/baz/*',
697+
'other/baz/',
698+
],
699+
},
700+
},
701+
},
702+
],
703+
Version: '2012-10-17',
704+
}
705+
);
706+
assert.deepStrictEqual(
707+
acceptResourceAccessMock.mock.calls[0].arguments[1],
708+
ssmEnvironmentEntriesStub
709+
);
710+
});
638711
});
639712
});
640713

packages/backend-storage/src/storage_access_orchestrator.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import {
66
import {
77
StorageAccessBuilder,
88
StorageAccessGenerator,
9-
StorageAction,
109
StoragePath,
1110
} from './types.js';
1211
import { ownerPathPartToken } from './constants.js';
1312
import { StorageAccessPolicyFactory } from './storage_access_policy_factory.js';
1413
import { validateStorageAccessPaths as _validateStorageAccessPaths } from './validate_storage_access_paths.js';
1514
import { roleAccessBuilder as _roleAccessBuilder } from './access_builder.js';
15+
import { InternalStorageAction } from './private_types.js';
1616

1717
/* some types internal to this file to improve readability */
1818

@@ -36,7 +36,7 @@ export class StorageAccessOrchestrator {
3636
{
3737
acceptor: ResourceAccessAcceptor;
3838
accessMap: Map<
39-
StorageAction,
39+
InternalStorageAction,
4040
{ allow: Set<StoragePath>; deny: Set<StoragePath> }
4141
>;
4242
}
@@ -104,10 +104,20 @@ export class StorageAccessOrchestrator {
104104
permission.ownerPlaceholderSubstitution
105105
) as StoragePath;
106106

107+
// replace "read" with "get" and "list" in actions
108+
const replaceReadWithGetAndList = permission.actions.flatMap(
109+
(action) => (action === 'read' ? ['get', 'list'] : [action])
110+
) as InternalStorageAction[];
111+
112+
// ensure the actions list has no duplicates
113+
const noDuplicateActions = Array.from(
114+
new Set(replaceReadWithGetAndList)
115+
);
116+
107117
// set an entry that maps this permission to the resource acceptor
108118
this.addAccessDefinition(
109119
resourceAccessAcceptor,
110-
permission.actions,
120+
noDuplicateActions,
111121
prefix
112122
);
113123
});
@@ -124,7 +134,7 @@ export class StorageAccessOrchestrator {
124134
*/
125135
private addAccessDefinition = (
126136
resourceAccessAcceptor: ResourceAccessAcceptor,
127-
actions: StorageAction[],
137+
actions: InternalStorageAction[],
128138
s3Prefix: StoragePath
129139
) => {
130140
const acceptorToken = resourceAccessAcceptor.identifier;

0 commit comments

Comments
 (0)