Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storage Browser Default Auth #13866

Merged
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>>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The in-line type is a bit hard the follow, what about using a interface with property names:

type Permission = string;

interface StorageBucketPaths {
  [pathPrefix: string]: {
    [accessType: string]: Permission[] | undefined;
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our ESLint does not allow for index signature. But i think we can create alias and align if we want. I can look into this in my follow up PR for pagination if there are no other comments :)

}
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 => {
calebpollman marked this conversation as resolved.
Show resolved Hide resolved
const listLocations = createAmplifyListLocationsHandler();

return { listLocations };
};
Loading
Loading