Skip to content

Commit

Permalink
Storage Browser Default Auth (#13866)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ashika112 authored Oct 18, 2024
1 parent 05ef3d8 commit e515d24
Show file tree
Hide file tree
Showing 14 changed files with 503 additions and 13 deletions.
53 changes: 53 additions & 0 deletions packages/core/__tests__/parseAmplifyOutputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,59 @@ describe('parseAmplifyOutputs tests', () => {

expect(() => parseAmplifyOutputs(amplifyOutputs)).toThrow();
});
it('should parse storage bucket with paths', () => {
const amplifyOutputs: AmplifyOutputs = {
version: '1.2',
storage: {
aws_region: 'us-west-2',
bucket_name: 'storage-bucket-test',
buckets: [
{
name: 'default-bucket',
bucket_name: 'storage-bucket-test',
aws_region: 'us-west-2',
paths: {
'other/*': {
guest: ['get', 'list'],
authenticated: ['get', 'list', 'write'],
},
'admin/*': {
groupsauditor: ['get', 'list'],
groupsadmin: ['get', 'list', 'write', 'delete'],
},
},
},
],
},
};

const result = parseAmplifyOutputs(amplifyOutputs);

expect(result).toEqual({
Storage: {
S3: {
bucket: 'storage-bucket-test',
region: 'us-west-2',
buckets: {
'default-bucket': {
bucketName: 'storage-bucket-test',
region: 'us-west-2',
paths: {
'other/*': {
guest: ['get', 'list'],
authenticated: ['get', 'list', 'write'],
},
'admin/*': {
groupsauditor: ['get', 'list'],
groupsadmin: ['get', 'list', 'write', 'delete'],
},
},
},
},
},
},
});
});
});

describe('analytics tests', () => {
Expand Down
27 changes: 15 additions & 12 deletions packages/core/src/parseAmplifyOutputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,18 +342,21 @@ function createBucketInfoMap(
): Record<string, BucketInfo> {
const mappedBuckets: Record<string, BucketInfo> = {};

buckets.forEach(({ name, bucket_name: bucketName, aws_region: region }) => {
if (name in mappedBuckets) {
throw new Error(
`Duplicate friendly name found: ${name}. Name must be unique.`,
);
}

mappedBuckets[name] = {
bucketName,
region,
};
});
buckets.forEach(
({ name, bucket_name: bucketName, aws_region: region, paths }) => {
if (name in mappedBuckets) {
throw new Error(
`Duplicate friendly name found: ${name}. Name must be unique.`,
);
}

mappedBuckets[name] = {
bucketName,
region,
paths,
};
},
);

return mappedBuckets;
}
2 changes: 2 additions & 0 deletions packages/core/src/singleton/AmplifyOutputs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export interface AmplifyOutputsStorageBucketProperties {
bucket_name: string;
/** Region for the bucket */
aws_region: string;
/** Paths to object with access permissions */
paths?: Record<string, Record<string, string[] | undefined>>;
}
export interface AmplifyOutputsStorageProperties {
/** Default region for Storage */
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/singleton/Storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface BucketInfo {
bucketName: string;
/** Region of the bucket */
region: string;
/** Paths to object with access permissions */
paths?: Record<string, Record<string, string[] | undefined>>;
}
export interface S3ProviderConfig {
S3: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { Amplify, fetchAuthSession } from '@aws-amplify/core';

import { resolveLocationsForCurrentSession } from '../../../src/internals/amplifyAuthConfigAdapter/resolveLocationsForCurrentSession';
import { createAmplifyAuthConfigAdapter } from '../../../src/internals';

jest.mock('@aws-amplify/core', () => ({
ConsoleLogger: jest.fn(),
Amplify: {
getConfig: jest.fn(),
Auth: {
getConfig: jest.fn(),
fetchAuthSession: jest.fn(),
},
},
fetchAuthSession: jest.fn(),
}));
jest.mock(
'../../../src/internals/amplifyAuthConfigAdapter/resolveLocationsForCurrentSession',
);

const credentials = {
accessKeyId: 'accessKeyId',
sessionToken: 'sessionToken',
secretAccessKey: 'secretAccessKey',
};
const identityId = 'identityId';

const mockGetConfig = jest.mocked(Amplify.getConfig);
const mockFetchAuthSession = fetchAuthSession as jest.Mock;
const mockResolveLocationsFromCurrentSession =
resolveLocationsForCurrentSession as jest.Mock;

describe('createAmplifyAuthConfigAdapter', () => {
beforeEach(() => {
jest.clearAllMocks();
});

mockGetConfig.mockReturnValue({
Storage: {
S3: {
bucket: 'bucket1',
region: 'region1',
buckets: {
'bucket-1': {
bucketName: 'bucket-1',
region: 'region1',
paths: {},
},
},
},
},
});
mockFetchAuthSession.mockResolvedValue({
credentials,
identityId,
tokens: {
accessToken: { payload: {} },
},
});

it('should return an AuthConfigAdapter with listLocations function', async () => {
const adapter = createAmplifyAuthConfigAdapter();
expect(adapter).toHaveProperty('listLocations');
const { listLocations } = adapter;
await listLocations();
expect(mockFetchAuthSession).toHaveBeenCalled();
});

it('should return empty locations when buckets are not defined', async () => {
mockGetConfig.mockReturnValue({ Storage: { S3: { buckets: undefined } } });

const adapter = createAmplifyAuthConfigAdapter();
const result = await adapter.listLocations();

expect(result).toEqual({ locations: [] });
});

it('should generate locations correctly when buckets are defined', async () => {
const mockBuckets = {
bucket1: {
bucketName: 'bucket1',
region: 'region1',
paths: {
'/path1': {
entityidentity: ['read', 'write'],
groupsadmin: ['read'],
},
},
},
};

mockGetConfig.mockReturnValue({
Storage: { S3: { buckets: mockBuckets } },
});
mockResolveLocationsFromCurrentSession.mockReturnValue([
{
type: 'PREFIX',
permission: ['read', 'write'],
scope: {
bucketName: 'bucket1',
path: '/path1',
},
},
]);

const adapter = createAmplifyAuthConfigAdapter();
const result = await adapter.listLocations();

expect(result).toEqual({
locations: [
{
type: 'PREFIX',
permission: ['read', 'write'],
scope: {
bucketName: 'bucket1',
path: '/path1',
},
},
],
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { resolveLocationsForCurrentSession } from '../../../src/internals/amplifyAuthConfigAdapter/resolveLocationsForCurrentSession';
import { BucketInfo } from '../../../src/providers/s3/types/options';

describe('resolveLocationsForCurrentSession', () => {
const mockBuckets: Record<string, BucketInfo> = {
bucket1: {
bucketName: 'bucket1',
region: 'region1',
paths: {
'path1/*': {
guest: ['get', 'list'],
authenticated: ['get', 'list', 'write'],
},
'path2/*': {
groupsauditor: ['get', 'list'],
groupsadmin: ['get', 'list', 'write', 'delete'],
},
// eslint-disable-next-line no-template-curly-in-string
'profile-pictures/${cognito-identity.amazonaws.com:sub}/*': {
entityidentity: ['get', 'list', 'write', 'delete'],
},
},
},
bucket2: {
bucketName: 'bucket2',
region: 'region1',
paths: {
'path3/*': {
guest: ['read'],
},
},
},
};

it('should generate locations correctly when tokens are true', () => {
const result = resolveLocationsForCurrentSession({
buckets: mockBuckets,
isAuthenticated: true,
identityId: '12345',
userGroup: 'admin',
});

expect(result).toEqual([
{
type: 'PREFIX',
permission: ['get', 'list', 'write'],
bucket: 'bucket1',
prefix: 'path1/*',
},
{
type: 'PREFIX',
permission: ['get', 'list', 'write', 'delete'],
bucket: 'bucket1',
prefix: 'path2/*',
},
{
type: 'PREFIX',
permission: ['get', 'list', 'write', 'delete'],
bucket: 'bucket1',
prefix: 'profile-pictures/12345/*',
},
]);
});

it('should generate locations correctly when tokens are true & bad userGroup', () => {
const result = resolveLocationsForCurrentSession({
buckets: mockBuckets,
isAuthenticated: true,
identityId: '12345',
userGroup: 'editor',
});

expect(result).toEqual([
{
type: 'PREFIX',
permission: ['get', 'list', 'write'],
bucket: 'bucket1',
prefix: 'path1/*',
},
{
type: 'PREFIX',
permission: ['get', 'list', 'write', 'delete'],
bucket: 'bucket1',
prefix: 'profile-pictures/12345/*',
},
]);
});

it('should continue to next bucket when paths are not defined', () => {
const result = resolveLocationsForCurrentSession({
buckets: {
bucket1: {
bucketName: 'bucket1',
region: 'region1',
paths: undefined,
},
bucket2: {
bucketName: 'bucket1',
region: 'region1',
paths: {
'path1/*': {
guest: ['get', 'list'],
authenticated: ['get', 'list', 'write'],
},
},
},
},
isAuthenticated: true,
identityId: '12345',
userGroup: 'admin',
});

expect(result).toEqual([
{
type: 'PREFIX',
permission: ['get', 'list', 'write'],
bucket: 'bucket1',
prefix: 'path1/*',
},
]);
});

it('should generate locations correctly when tokens are false', () => {
const result = resolveLocationsForCurrentSession({
buckets: mockBuckets,
isAuthenticated: false,
});

expect(result).toEqual([
{
type: 'PREFIX',
permission: ['get', 'list'],
bucket: 'bucket1',
prefix: 'path1/*',
},
{
type: 'PREFIX',
permission: ['read'],
bucket: 'bucket2',
prefix: 'path3/*',
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { ListPaths } from '../types/credentials';

import { createAmplifyListLocationsHandler } from './createAmplifyListLocationsHandler';

export interface AuthConfigAdapter {
listLocations: ListPaths;
}

export const createAmplifyAuthConfigAdapter = (): AuthConfigAdapter => {
const listLocations = createAmplifyListLocationsHandler();

return { listLocations };
};
Loading

0 comments on commit e515d24

Please sign in to comment.