Skip to content

Commit 991cb01

Browse files
committed
feat(storage): complete list location handler and unit tests
1 parent d46660d commit 991cb01

File tree

7 files changed

+262
-29
lines changed

7 files changed

+262
-29
lines changed

packages/core/src/Platform/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export enum StorageAction {
121121
GetProperties = '6',
122122
GetUrl = '7',
123123
GetDataAccess = '8',
124+
ListCallerAccessGrants = '9',
124125
}
125126

126127
interface ActionMap {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
4+
import { listCallerAccessGrants } from '../../../src/storageBrowser/apis/listCallerAccessGrants';
5+
import { listCallerAccessGrants as listCallerAccessGrantsClient } from '../../../src/providers/s3/utils/client/s3control';
6+
7+
jest.mock('../../../src/providers/s3/utils/client/s3control');
8+
9+
const mockAccountId = '1234567890';
10+
const mockRegion = 'us-foo-2';
11+
const mockCredentialsProvider = jest.fn();
12+
const mockNextToken = '123';
13+
const mockPageSize = 123;
14+
15+
describe('listCallerAccessGrants', () => {
16+
afterEach(() => {
17+
jest.clearAllMocks();
18+
});
19+
20+
it('should invoke the listCallerAccessGrants client with expected parameters', async () => {
21+
expect.assertions(1);
22+
jest.mocked(listCallerAccessGrantsClient).mockResolvedValue({
23+
NextToken: undefined,
24+
CallerAccessGrantsList: [],
25+
$metadata: {} as any,
26+
});
27+
await listCallerAccessGrants({
28+
accountId: mockAccountId,
29+
region: mockRegion,
30+
credentialsProvider: mockCredentialsProvider,
31+
nextToken: mockNextToken,
32+
pageSize: mockPageSize,
33+
});
34+
expect(listCallerAccessGrantsClient).toHaveBeenCalledWith(
35+
expect.objectContaining({
36+
region: mockRegion,
37+
credentials: expect.any(Function),
38+
}),
39+
expect.objectContaining({
40+
AccountId: mockAccountId,
41+
NextToken: mockNextToken,
42+
MaxResults: mockPageSize,
43+
}),
44+
);
45+
});
46+
47+
it('should set a default page size', async () => {
48+
expect.assertions(1);
49+
jest.mocked(listCallerAccessGrantsClient).mockResolvedValue({
50+
NextToken: undefined,
51+
CallerAccessGrantsList: [],
52+
$metadata: {} as any,
53+
});
54+
await listCallerAccessGrants({
55+
accountId: mockAccountId,
56+
region: mockRegion,
57+
credentialsProvider: mockCredentialsProvider,
58+
});
59+
expect(listCallerAccessGrantsClient).toHaveBeenCalledWith(
60+
expect.anything(),
61+
expect.objectContaining({
62+
MaxResults: 1000,
63+
}),
64+
);
65+
});
66+
67+
it('should set response location type correctly', async () => {
68+
expect.assertions(2);
69+
jest.mocked(listCallerAccessGrantsClient).mockResolvedValue({
70+
NextToken: undefined,
71+
CallerAccessGrantsList: [
72+
{
73+
GrantScope: 's3://bucket/*',
74+
Permission: 'READ',
75+
},
76+
{
77+
GrantScope: 's3://bucket/path/*',
78+
Permission: 'READWRITE',
79+
},
80+
{
81+
GrantScope: 's3://bucket/path/to/object',
82+
Permission: 'READ',
83+
ApplicationArn: 'arn:123',
84+
},
85+
],
86+
$metadata: {} as any,
87+
});
88+
const { locations, nextToken } = await listCallerAccessGrants({
89+
accountId: mockAccountId,
90+
region: mockRegion,
91+
credentialsProvider: mockCredentialsProvider,
92+
});
93+
94+
expect(locations).toEqual([
95+
{
96+
scope: 's3://bucket/*',
97+
type: 'BUCKET',
98+
permission: 'READ',
99+
applicationArn: undefined,
100+
},
101+
{
102+
scope: 's3://bucket/path/*',
103+
type: 'PREFIX',
104+
permission: 'READWRITE',
105+
applicationArn: undefined,
106+
},
107+
{
108+
scope: 's3://bucket/path/to/object',
109+
type: 'OBJECT',
110+
permission: 'READ',
111+
applicationArn: 'arn:123',
112+
},
113+
]);
114+
expect(nextToken).toBeUndefined();
115+
});
116+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { createListLocationsHandler } from '../../../src/storageBrowser/managedAuthConfigAdapter/createListLocationsHandler';
5+
import { listCallerAccessGrants } from '../../../src/storageBrowser/apis/listCallerAccessGrants';
6+
7+
jest.mock('../../../src/storageBrowser/apis/listCallerAccessGrants');
8+
9+
jest.mocked(listCallerAccessGrants).mockResolvedValue({
10+
locations: [],
11+
});
12+
13+
describe('createListLocationsHandler', () => {
14+
it('should parse the underlying API with right parameters', async () => {
15+
const mockAccountId = '1234567890';
16+
const mockRegion = 'us-foo-1';
17+
const mockCredentialsProvider = jest.fn();
18+
const mockNextToken = '123';
19+
const mockPageSize = 123;
20+
const handler = createListLocationsHandler({
21+
accountId: mockAccountId,
22+
region: mockRegion,
23+
credentialsProvider: mockCredentialsProvider,
24+
});
25+
await handler({ nextToken: mockNextToken, pageSize: mockPageSize });
26+
expect(listCallerAccessGrants).toHaveBeenCalledWith({
27+
accountId: mockAccountId,
28+
region: mockRegion,
29+
credentialsProvider: mockCredentialsProvider,
30+
nextToken: mockNextToken,
31+
pageSize: mockPageSize,
32+
});
33+
});
34+
});

packages/storage/src/storageBrowser/apis/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
export const DEFAULT_CRED_TTL = 15 * 60; // 15 minutes
5+
export const MAX_PAGE_SIZE = 1000;
Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,105 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { StorageAction } from '@aws-amplify/core/internals/utils';
5+
6+
import { logger } from '../../utils';
7+
import { listCallerAccessGrants as listCallerAccessGrantsClient } from '../../providers/s3/utils/client/s3control';
8+
import { AccessGrant, LocationType, Permission } from '../types';
9+
import { StorageError } from '../../errors/StorageError';
10+
import { getStorageUserAgentValue } from '../../providers/s3/utils/userAgent';
11+
412
import {
513
ListCallerAccessGrantsInput,
614
ListCallerAccessGrantsOutput,
715
} from './types';
16+
import { MAX_PAGE_SIZE } from './constants';
817

9-
export const listCallerAccessGrants = (
10-
// eslint-disable-next-line unused-imports/no-unused-vars
18+
export const listCallerAccessGrants = async (
1119
input: ListCallerAccessGrantsInput,
1220
): Promise<ListCallerAccessGrantsOutput> => {
13-
// TODO(@AllanZhengYP)
14-
throw new Error('Not Implemented');
21+
const { credentialsProvider, accountId, region, nextToken, pageSize } = input;
22+
23+
logger.debug(`listing available locations from account ${input.accountId}`);
24+
25+
if (!!pageSize && pageSize > MAX_PAGE_SIZE) {
26+
logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`);
27+
}
28+
29+
const clientCredentialsProvider = async () => {
30+
const { credentials } = await credentialsProvider();
31+
32+
return credentials;
33+
};
34+
35+
const { CallerAccessGrantsList, NextToken } =
36+
await listCallerAccessGrantsClient(
37+
{
38+
credentials: clientCredentialsProvider,
39+
region,
40+
userAgentValue: getStorageUserAgentValue(
41+
StorageAction.ListCallerAccessGrants,
42+
),
43+
},
44+
{
45+
AccountId: accountId,
46+
NextToken: nextToken,
47+
MaxResults: pageSize ?? MAX_PAGE_SIZE,
48+
},
49+
);
50+
51+
const accessGrants: AccessGrant[] =
52+
CallerAccessGrantsList?.map(grant => {
53+
// These value correct from servers mostly but we add assertions to make TSC happy.
54+
assertPermission(grant.Permission);
55+
assertString(grant.GrantScope);
56+
57+
return {
58+
scope: grant.GrantScope,
59+
permission: grant.Permission,
60+
applicationArn: grant.ApplicationArn,
61+
type: parseGrantType(grant.GrantScope!),
62+
};
63+
}) ?? [];
64+
65+
return {
66+
locations: accessGrants,
67+
nextToken: NextToken,
68+
};
1569
};
70+
71+
const parseGrantType = (grantScope: string): LocationType => {
72+
const BucketScopeReg = /^s3:\/\/(.*)\/\*$/;
73+
const possibleBucketName = grantScope.match(BucketScopeReg)?.[1];
74+
if (!grantScope.endsWith('*')) {
75+
return 'OBJECT';
76+
} else if (
77+
grantScope.endsWith('/*') &&
78+
possibleBucketName &&
79+
possibleBucketName.indexOf('/') === -1
80+
) {
81+
return 'BUCKET';
82+
} else {
83+
return 'PREFIX';
84+
}
85+
};
86+
87+
function assertPermission(
88+
permissionValue: string | undefined,
89+
): asserts permissionValue is Permission {
90+
if (!['READ', 'READWRITE', 'WRITE'].includes(permissionValue ?? '')) {
91+
throw new StorageError({
92+
name: 'InvalidPermission',
93+
message: `Invalid permission: ${permissionValue}`,
94+
});
95+
}
96+
}
97+
98+
function assertString(value: unknown): asserts value is string {
99+
if (typeof value !== 'string') {
100+
throw new StorageError({
101+
name: 'InvalidString',
102+
message: `Expected string, got ${value}`,
103+
});
104+
}
105+
}

packages/storage/src/storageBrowser/apis/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import {
55
AccessGrant,
66
CredentialsProvider,
7+
ListLocationsInput,
78
ListLocationsOutput,
89
LocationCredentials,
910
Permission,
1011
PrefixType,
1112
Privilege,
1213
} from '../types';
1314

14-
export interface ListCallerAccessGrantsInput {
15+
export interface ListCallerAccessGrantsInput extends ListLocationsInput {
1516
accountId: string;
1617
credentialsProvider: CredentialsProvider;
1718
region: string;

packages/storage/src/storageBrowser/managedAuthConfigAdapter/createListLocationsHandler.ts

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { CredentialsProvider, ListLocations } from '../types';
5-
import { logger } from '../../utils';
6-
7-
declare const listCallerAccessGrants: (config: any, input: any) => Promise<any>;
8-
9-
const MAX_PAGE_SIZE = 1000;
5+
import { listCallerAccessGrants } from '../apis/listCallerAccessGrants';
106

117
interface CreateListLocationsHandlerInput {
128
accountId: string;
@@ -15,29 +11,23 @@ interface CreateListLocationsHandlerInput {
1511
}
1612

1713
export const createListLocationsHandler = (
18-
// eslint-disable-next-line unused-imports/no-unused-vars
1914
input: CreateListLocationsHandlerInput,
2015
): ListLocations => {
21-
const config = {
22-
credentials: input.credentialsProvider,
23-
region: input.region,
24-
};
25-
2616
return async (handlerInput = {}) => {
2717
const { nextToken, pageSize } = handlerInput;
28-
logger.debug(`list available locations from account ${input.accountId}`);
29-
const params = {
30-
AccountId: input.accountId,
31-
NextToken: nextToken,
32-
MaxKeys: pageSize,
33-
};
34-
if (!!pageSize && pageSize > MAX_PAGE_SIZE) {
35-
logger.debug(`defaulting pageSize to ${MAX_PAGE_SIZE}.`);
36-
params.MaxKeys = MAX_PAGE_SIZE;
37-
}
38-
const { result } = await listCallerAccessGrants(config, params);
18+
const { locations, nextToken: newNextToken } = await listCallerAccessGrants(
19+
{
20+
accountId: input.accountId,
21+
credentialsProvider: input.credentialsProvider,
22+
region: input.region,
23+
pageSize,
24+
nextToken,
25+
},
26+
);
3927

40-
// TODO
41-
return result;
28+
return {
29+
locations,
30+
nextToken: newNextToken || undefined,
31+
};
4232
};
4333
};

0 commit comments

Comments
 (0)