Skip to content

Commit e515d24

Browse files
authored
Storage Browser Default Auth (#13866)
* first draft poc * upadtes * add listPaths API * update new file structure * fix types * refactor types and utils * update tests * fix test * fix bundle size test * update the listLocation handler * rename util * update Path type * fix missed type
1 parent 05ef3d8 commit e515d24

File tree

14 files changed

+503
-13
lines changed

14 files changed

+503
-13
lines changed

packages/core/__tests__/parseAmplifyOutputs.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,59 @@ describe('parseAmplifyOutputs tests', () => {
294294

295295
expect(() => parseAmplifyOutputs(amplifyOutputs)).toThrow();
296296
});
297+
it('should parse storage bucket with paths', () => {
298+
const amplifyOutputs: AmplifyOutputs = {
299+
version: '1.2',
300+
storage: {
301+
aws_region: 'us-west-2',
302+
bucket_name: 'storage-bucket-test',
303+
buckets: [
304+
{
305+
name: 'default-bucket',
306+
bucket_name: 'storage-bucket-test',
307+
aws_region: 'us-west-2',
308+
paths: {
309+
'other/*': {
310+
guest: ['get', 'list'],
311+
authenticated: ['get', 'list', 'write'],
312+
},
313+
'admin/*': {
314+
groupsauditor: ['get', 'list'],
315+
groupsadmin: ['get', 'list', 'write', 'delete'],
316+
},
317+
},
318+
},
319+
],
320+
},
321+
};
322+
323+
const result = parseAmplifyOutputs(amplifyOutputs);
324+
325+
expect(result).toEqual({
326+
Storage: {
327+
S3: {
328+
bucket: 'storage-bucket-test',
329+
region: 'us-west-2',
330+
buckets: {
331+
'default-bucket': {
332+
bucketName: 'storage-bucket-test',
333+
region: 'us-west-2',
334+
paths: {
335+
'other/*': {
336+
guest: ['get', 'list'],
337+
authenticated: ['get', 'list', 'write'],
338+
},
339+
'admin/*': {
340+
groupsauditor: ['get', 'list'],
341+
groupsadmin: ['get', 'list', 'write', 'delete'],
342+
},
343+
},
344+
},
345+
},
346+
},
347+
},
348+
});
349+
});
297350
});
298351

299352
describe('analytics tests', () => {

packages/core/src/parseAmplifyOutputs.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -342,18 +342,21 @@ function createBucketInfoMap(
342342
): Record<string, BucketInfo> {
343343
const mappedBuckets: Record<string, BucketInfo> = {};
344344

345-
buckets.forEach(({ name, bucket_name: bucketName, aws_region: region }) => {
346-
if (name in mappedBuckets) {
347-
throw new Error(
348-
`Duplicate friendly name found: ${name}. Name must be unique.`,
349-
);
350-
}
351-
352-
mappedBuckets[name] = {
353-
bucketName,
354-
region,
355-
};
356-
});
345+
buckets.forEach(
346+
({ name, bucket_name: bucketName, aws_region: region, paths }) => {
347+
if (name in mappedBuckets) {
348+
throw new Error(
349+
`Duplicate friendly name found: ${name}. Name must be unique.`,
350+
);
351+
}
352+
353+
mappedBuckets[name] = {
354+
bucketName,
355+
region,
356+
paths,
357+
};
358+
},
359+
);
357360

358361
return mappedBuckets;
359362
}

packages/core/src/singleton/AmplifyOutputs/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export interface AmplifyOutputsStorageBucketProperties {
5050
bucket_name: string;
5151
/** Region for the bucket */
5252
aws_region: string;
53+
/** Paths to object with access permissions */
54+
paths?: Record<string, Record<string, string[] | undefined>>;
5355
}
5456
export interface AmplifyOutputsStorageProperties {
5557
/** Default region for Storage */

packages/core/src/singleton/Storage/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface BucketInfo {
1212
bucketName: string;
1313
/** Region of the bucket */
1414
region: string;
15+
/** Paths to object with access permissions */
16+
paths?: Record<string, Record<string, string[] | undefined>>;
1517
}
1618
export interface S3ProviderConfig {
1719
S3: {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Amplify, fetchAuthSession } from '@aws-amplify/core';
5+
6+
import { resolveLocationsForCurrentSession } from '../../../src/internals/amplifyAuthConfigAdapter/resolveLocationsForCurrentSession';
7+
import { createAmplifyAuthConfigAdapter } from '../../../src/internals';
8+
9+
jest.mock('@aws-amplify/core', () => ({
10+
ConsoleLogger: jest.fn(),
11+
Amplify: {
12+
getConfig: jest.fn(),
13+
Auth: {
14+
getConfig: jest.fn(),
15+
fetchAuthSession: jest.fn(),
16+
},
17+
},
18+
fetchAuthSession: jest.fn(),
19+
}));
20+
jest.mock(
21+
'../../../src/internals/amplifyAuthConfigAdapter/resolveLocationsForCurrentSession',
22+
);
23+
24+
const credentials = {
25+
accessKeyId: 'accessKeyId',
26+
sessionToken: 'sessionToken',
27+
secretAccessKey: 'secretAccessKey',
28+
};
29+
const identityId = 'identityId';
30+
31+
const mockGetConfig = jest.mocked(Amplify.getConfig);
32+
const mockFetchAuthSession = fetchAuthSession as jest.Mock;
33+
const mockResolveLocationsFromCurrentSession =
34+
resolveLocationsForCurrentSession as jest.Mock;
35+
36+
describe('createAmplifyAuthConfigAdapter', () => {
37+
beforeEach(() => {
38+
jest.clearAllMocks();
39+
});
40+
41+
mockGetConfig.mockReturnValue({
42+
Storage: {
43+
S3: {
44+
bucket: 'bucket1',
45+
region: 'region1',
46+
buckets: {
47+
'bucket-1': {
48+
bucketName: 'bucket-1',
49+
region: 'region1',
50+
paths: {},
51+
},
52+
},
53+
},
54+
},
55+
});
56+
mockFetchAuthSession.mockResolvedValue({
57+
credentials,
58+
identityId,
59+
tokens: {
60+
accessToken: { payload: {} },
61+
},
62+
});
63+
64+
it('should return an AuthConfigAdapter with listLocations function', async () => {
65+
const adapter = createAmplifyAuthConfigAdapter();
66+
expect(adapter).toHaveProperty('listLocations');
67+
const { listLocations } = adapter;
68+
await listLocations();
69+
expect(mockFetchAuthSession).toHaveBeenCalled();
70+
});
71+
72+
it('should return empty locations when buckets are not defined', async () => {
73+
mockGetConfig.mockReturnValue({ Storage: { S3: { buckets: undefined } } });
74+
75+
const adapter = createAmplifyAuthConfigAdapter();
76+
const result = await adapter.listLocations();
77+
78+
expect(result).toEqual({ locations: [] });
79+
});
80+
81+
it('should generate locations correctly when buckets are defined', async () => {
82+
const mockBuckets = {
83+
bucket1: {
84+
bucketName: 'bucket1',
85+
region: 'region1',
86+
paths: {
87+
'/path1': {
88+
entityidentity: ['read', 'write'],
89+
groupsadmin: ['read'],
90+
},
91+
},
92+
},
93+
};
94+
95+
mockGetConfig.mockReturnValue({
96+
Storage: { S3: { buckets: mockBuckets } },
97+
});
98+
mockResolveLocationsFromCurrentSession.mockReturnValue([
99+
{
100+
type: 'PREFIX',
101+
permission: ['read', 'write'],
102+
scope: {
103+
bucketName: 'bucket1',
104+
path: '/path1',
105+
},
106+
},
107+
]);
108+
109+
const adapter = createAmplifyAuthConfigAdapter();
110+
const result = await adapter.listLocations();
111+
112+
expect(result).toEqual({
113+
locations: [
114+
{
115+
type: 'PREFIX',
116+
permission: ['read', 'write'],
117+
scope: {
118+
bucketName: 'bucket1',
119+
path: '/path1',
120+
},
121+
},
122+
],
123+
});
124+
});
125+
});
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { resolveLocationsForCurrentSession } from '../../../src/internals/amplifyAuthConfigAdapter/resolveLocationsForCurrentSession';
2+
import { BucketInfo } from '../../../src/providers/s3/types/options';
3+
4+
describe('resolveLocationsForCurrentSession', () => {
5+
const mockBuckets: Record<string, BucketInfo> = {
6+
bucket1: {
7+
bucketName: 'bucket1',
8+
region: 'region1',
9+
paths: {
10+
'path1/*': {
11+
guest: ['get', 'list'],
12+
authenticated: ['get', 'list', 'write'],
13+
},
14+
'path2/*': {
15+
groupsauditor: ['get', 'list'],
16+
groupsadmin: ['get', 'list', 'write', 'delete'],
17+
},
18+
// eslint-disable-next-line no-template-curly-in-string
19+
'profile-pictures/${cognito-identity.amazonaws.com:sub}/*': {
20+
entityidentity: ['get', 'list', 'write', 'delete'],
21+
},
22+
},
23+
},
24+
bucket2: {
25+
bucketName: 'bucket2',
26+
region: 'region1',
27+
paths: {
28+
'path3/*': {
29+
guest: ['read'],
30+
},
31+
},
32+
},
33+
};
34+
35+
it('should generate locations correctly when tokens are true', () => {
36+
const result = resolveLocationsForCurrentSession({
37+
buckets: mockBuckets,
38+
isAuthenticated: true,
39+
identityId: '12345',
40+
userGroup: 'admin',
41+
});
42+
43+
expect(result).toEqual([
44+
{
45+
type: 'PREFIX',
46+
permission: ['get', 'list', 'write'],
47+
bucket: 'bucket1',
48+
prefix: 'path1/*',
49+
},
50+
{
51+
type: 'PREFIX',
52+
permission: ['get', 'list', 'write', 'delete'],
53+
bucket: 'bucket1',
54+
prefix: 'path2/*',
55+
},
56+
{
57+
type: 'PREFIX',
58+
permission: ['get', 'list', 'write', 'delete'],
59+
bucket: 'bucket1',
60+
prefix: 'profile-pictures/12345/*',
61+
},
62+
]);
63+
});
64+
65+
it('should generate locations correctly when tokens are true & bad userGroup', () => {
66+
const result = resolveLocationsForCurrentSession({
67+
buckets: mockBuckets,
68+
isAuthenticated: true,
69+
identityId: '12345',
70+
userGroup: 'editor',
71+
});
72+
73+
expect(result).toEqual([
74+
{
75+
type: 'PREFIX',
76+
permission: ['get', 'list', 'write'],
77+
bucket: 'bucket1',
78+
prefix: 'path1/*',
79+
},
80+
{
81+
type: 'PREFIX',
82+
permission: ['get', 'list', 'write', 'delete'],
83+
bucket: 'bucket1',
84+
prefix: 'profile-pictures/12345/*',
85+
},
86+
]);
87+
});
88+
89+
it('should continue to next bucket when paths are not defined', () => {
90+
const result = resolveLocationsForCurrentSession({
91+
buckets: {
92+
bucket1: {
93+
bucketName: 'bucket1',
94+
region: 'region1',
95+
paths: undefined,
96+
},
97+
bucket2: {
98+
bucketName: 'bucket1',
99+
region: 'region1',
100+
paths: {
101+
'path1/*': {
102+
guest: ['get', 'list'],
103+
authenticated: ['get', 'list', 'write'],
104+
},
105+
},
106+
},
107+
},
108+
isAuthenticated: true,
109+
identityId: '12345',
110+
userGroup: 'admin',
111+
});
112+
113+
expect(result).toEqual([
114+
{
115+
type: 'PREFIX',
116+
permission: ['get', 'list', 'write'],
117+
bucket: 'bucket1',
118+
prefix: 'path1/*',
119+
},
120+
]);
121+
});
122+
123+
it('should generate locations correctly when tokens are false', () => {
124+
const result = resolveLocationsForCurrentSession({
125+
buckets: mockBuckets,
126+
isAuthenticated: false,
127+
});
128+
129+
expect(result).toEqual([
130+
{
131+
type: 'PREFIX',
132+
permission: ['get', 'list'],
133+
bucket: 'bucket1',
134+
prefix: 'path1/*',
135+
},
136+
{
137+
type: 'PREFIX',
138+
permission: ['read'],
139+
bucket: 'bucket2',
140+
prefix: 'path3/*',
141+
},
142+
]);
143+
});
144+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { ListPaths } from '../types/credentials';
5+
6+
import { createAmplifyListLocationsHandler } from './createAmplifyListLocationsHandler';
7+
8+
export interface AuthConfigAdapter {
9+
listLocations: ListPaths;
10+
}
11+
12+
export const createAmplifyAuthConfigAdapter = (): AuthConfigAdapter => {
13+
const listLocations = createAmplifyListLocationsHandler();
14+
15+
return { listLocations };
16+
};

0 commit comments

Comments
 (0)