From e44d290313a4b2f7443d55380282193499a7abde Mon Sep 17 00:00:00 2001 From: Jim Blanchard Date: Tue, 16 Jul 2024 20:05:09 -0500 Subject: [PATCH] feat: Implement getLocationCredentials handler & integrate with adapter (#13600) --- packages/aws-amplify/package.json | 12 +- packages/core/src/Platform/types.ts | 1 + .../storageBrowser/apis/getDataAccess.test.ts | 116 ++++++++++++++++++ .../src/storageBrowser/apis/constants.ts | 4 + .../src/storageBrowser/apis/getDataAccess.ts | 66 ++++++++++ .../{ => apis}/listCallerAccessGrants.ts | 13 +- .../storage/src/storageBrowser/apis/types.ts | 33 +++++ .../createLocationCredentialsHandler.ts | 32 ++++- .../createManagedAuthConfigAdapter.ts | 1 + packages/storage/src/storageBrowser/types.ts | 29 ++++- 10 files changed, 284 insertions(+), 23 deletions(-) create mode 100644 packages/storage/__tests__/storageBrowser/apis/getDataAccess.test.ts create mode 100644 packages/storage/src/storageBrowser/apis/constants.ts create mode 100644 packages/storage/src/storageBrowser/apis/getDataAccess.ts rename packages/storage/src/storageBrowser/{ => apis}/listCallerAccessGrants.ts (55%) create mode 100644 packages/storage/src/storageBrowser/apis/types.ts diff --git a/packages/aws-amplify/package.json b/packages/aws-amplify/package.json index d4cfecc01d7..ee7960ade9a 100644 --- a/packages/aws-amplify/package.json +++ b/packages/aws-amplify/package.json @@ -293,7 +293,7 @@ "name": "[Analytics] record (Pinpoint)", "path": "./dist/esm/analytics/index.mjs", "import": "{ record }", - "limit": "17.09 kB" + "limit": "17.11 kB" }, { "name": "[Analytics] record (Kinesis)", @@ -317,7 +317,7 @@ "name": "[Analytics] identifyUser (Pinpoint)", "path": "./dist/esm/analytics/index.mjs", "import": "{ identifyUser }", - "limit": "15.59 kB" + "limit": "15.60 kB" }, { "name": "[Analytics] enable", @@ -383,7 +383,7 @@ "name": "[Auth] confirmSignIn (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ confirmSignIn }", - "limit": "28.27 kB" + "limit": "28.28 kB" }, { "name": "[Auth] updateMFAPreference (Cognito)", @@ -401,7 +401,7 @@ "name": "[Auth] verifyTOTPSetup (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ verifyTOTPSetup }", - "limit": "12.6 kB" + "limit": "12.7 kB" }, { "name": "[Auth] updatePassword (Cognito)", @@ -449,13 +449,13 @@ "name": "[Auth] Basic Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signIn, signOut, fetchAuthSession, confirmSignIn }", - "limit": "30.06 kB" + "limit": "30.07 kB" }, { "name": "[Auth] OAuth Auth Flow (Cognito)", "path": "./dist/esm/auth/index.mjs", "import": "{ signInWithRedirect, signOut, fetchAuthSession }", - "limit": "21.47 kB" + "limit": "21.49 kB" }, { "name": "[Storage] copy (S3)", diff --git a/packages/core/src/Platform/types.ts b/packages/core/src/Platform/types.ts index 96ca1de77ec..aa49d1f06b6 100644 --- a/packages/core/src/Platform/types.ts +++ b/packages/core/src/Platform/types.ts @@ -120,6 +120,7 @@ export enum StorageAction { Remove = '5', GetProperties = '6', GetUrl = '7', + GetDataAccess = '8', } interface ActionMap { diff --git a/packages/storage/__tests__/storageBrowser/apis/getDataAccess.test.ts b/packages/storage/__tests__/storageBrowser/apis/getDataAccess.test.ts new file mode 100644 index 00000000000..0753e0ae334 --- /dev/null +++ b/packages/storage/__tests__/storageBrowser/apis/getDataAccess.test.ts @@ -0,0 +1,116 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { getDataAccess } from '../../../src/storageBrowser/apis/getDataAccess'; +import { getDataAccess as getDataAccessClient } from '../../../src/providers/s3/utils/client/s3control'; +import { GetDataAccessInput } from '../../../src/storageBrowser/apis/types'; + +jest.mock('../../../src/providers/s3/utils/client/s3control'); + +const MOCK_ACCOUNT_ID = 'accountId'; +const MOCK_REGION = 'us-east-2'; +const MOCK_ACCESS_ID = 'accessId'; +const MOCK_SECRET_ACCESS_KEY = 'secretAccessKey'; +const MOCK_SESSION_TOKEN = 'sessionToken'; +const MOCK_EXPIRATION = '2013-09-17T18:07:53.000Z'; +const MOCK_EXPIRATION_DATE = new Date(MOCK_EXPIRATION); +const MOCK_SCOPE = 's3://mybucket/files/*'; +const MOCK_CREDENTIALS = { + credentials: { + accessKeyId: MOCK_ACCESS_ID, + secretAccessKey: MOCK_SECRET_ACCESS_KEY, + sessionToken: MOCK_SESSION_TOKEN, + expiration: MOCK_EXPIRATION_DATE, + }, +}; +const MOCK_ACCESS_CREDENTIALS = { + AccessKeyId: MOCK_ACCESS_ID, + SecretAccessKey: MOCK_SECRET_ACCESS_KEY, + SessionToken: MOCK_SESSION_TOKEN, + Expiration: MOCK_EXPIRATION_DATE, +}; +const MOCK_CREDENTIAL_PROVIDER = async () => MOCK_CREDENTIALS; + +const sharedGetDataAccessParams: GetDataAccessInput = { + accountId: MOCK_ACCOUNT_ID, + credentialsProvider: MOCK_CREDENTIAL_PROVIDER, + durationSeconds: 900, + permission: 'READWRITE', + region: MOCK_REGION, + scope: MOCK_SCOPE, +}; + +describe('getDataAccess', () => { + const getDataAccessClientMock = getDataAccessClient as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + + getDataAccessClientMock.mockResolvedValue({ + Credentials: MOCK_ACCESS_CREDENTIALS, + MatchedGrantTarget: MOCK_SCOPE, + }); + }); + + it('should invoke the getDataAccess client correctly', async () => { + const result = await getDataAccess(sharedGetDataAccessParams); + + expect(getDataAccessClientMock).toHaveBeenCalledWith( + expect.objectContaining({ + credentials: MOCK_CREDENTIALS.credentials, + region: MOCK_REGION, + userAgentValue: expect.stringContaining('storage/8'), + }), + expect.objectContaining({ + AccountId: MOCK_ACCOUNT_ID, + Target: MOCK_SCOPE, + Permission: 'READWRITE', + TargetType: undefined, + DurationSeconds: 900, + }), + ); + + expect(result.credentials).toEqual(MOCK_CREDENTIALS.credentials); + expect(result.scope).toEqual(MOCK_SCOPE); + }); + + it('should throw an error if the service does not return credentials', async () => { + expect.assertions(1); + + getDataAccessClientMock.mockResolvedValue({ + Credentials: undefined, + MatchedGrantTarget: MOCK_SCOPE, + }); + + expect(getDataAccess(sharedGetDataAccessParams)).rejects.toThrow( + 'Service did not return credentials.', + ); + }); + + it('should set the correct target type when accessing an object', async () => { + const MOCK_OBJECT_SCOPE = 's3://mybucket/files/file.md'; + + getDataAccessClientMock.mockResolvedValue({ + Credentials: MOCK_ACCESS_CREDENTIALS, + MatchedGrantTarget: MOCK_OBJECT_SCOPE, + }); + + const result = await getDataAccess({ + ...sharedGetDataAccessParams, + scope: MOCK_OBJECT_SCOPE, + }); + + expect(getDataAccessClientMock).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + AccountId: MOCK_ACCOUNT_ID, + Target: MOCK_OBJECT_SCOPE, + Permission: 'READWRITE', + TargetType: 'Object', + DurationSeconds: 900, + }), + ); + + expect(result.scope).toEqual(MOCK_OBJECT_SCOPE); + }); +}); diff --git a/packages/storage/src/storageBrowser/apis/constants.ts b/packages/storage/src/storageBrowser/apis/constants.ts new file mode 100644 index 00000000000..e333ac5a5e2 --- /dev/null +++ b/packages/storage/src/storageBrowser/apis/constants.ts @@ -0,0 +1,4 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const DEFAULT_CRED_TTL = 15 * 60; // 15 minutes diff --git a/packages/storage/src/storageBrowser/apis/getDataAccess.ts b/packages/storage/src/storageBrowser/apis/getDataAccess.ts new file mode 100644 index 00000000000..5e5bec23540 --- /dev/null +++ b/packages/storage/src/storageBrowser/apis/getDataAccess.ts @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AmplifyErrorCode, + StorageAction, +} from '@aws-amplify/core/internals/utils'; + +import { getStorageUserAgentValue } from '../../providers/s3/utils/userAgent'; +import { getDataAccess as getDataAccessClient } from '../../providers/s3/utils/client/s3control'; +import { StorageError } from '../../errors/StorageError'; +import { logger } from '../../utils'; + +import { GetDataAccessInput, GetDataAccessOutput } from './types'; +import { DEFAULT_CRED_TTL } from './constants'; + +export const getDataAccess = async ( + input: GetDataAccessInput, +): Promise => { + const targetType = input.scope.endsWith('*') ? undefined : 'Object'; + const { credentials } = await input.credentialsProvider(); + + const result = await getDataAccessClient( + { + credentials, + region: input.region, + userAgentValue: getStorageUserAgentValue(StorageAction.GetDataAccess), + }, + { + AccountId: input.accountId, + Target: input.scope, + Permission: input.permission, + TargetType: targetType, + DurationSeconds: DEFAULT_CRED_TTL, + }, + ); + + const grantCredentials = result.Credentials; + + // Ensure that S3 returned credentials (this shouldn't happen) + if (!grantCredentials) { + throw new StorageError({ + name: AmplifyErrorCode.Unknown, + message: 'Service did not return credentials.', + }); + } else { + logger.debug(`Retrieved credentials for: ${result.MatchedGrantTarget}`); + } + + const { + AccessKeyId: accessKeyId, + SecretAccessKey: secretAccessKey, + SessionToken: sessionToken, + Expiration: expiration, + } = grantCredentials; + + return { + credentials: { + accessKeyId: accessKeyId!, + secretAccessKey: secretAccessKey!, + sessionToken, + expiration, + }, + scope: result.MatchedGrantTarget, + }; +}; diff --git a/packages/storage/src/storageBrowser/listCallerAccessGrants.ts b/packages/storage/src/storageBrowser/apis/listCallerAccessGrants.ts similarity index 55% rename from packages/storage/src/storageBrowser/listCallerAccessGrants.ts rename to packages/storage/src/storageBrowser/apis/listCallerAccessGrants.ts index c55b0ea75e8..dbe8b305b36 100644 --- a/packages/storage/src/storageBrowser/listCallerAccessGrants.ts +++ b/packages/storage/src/storageBrowser/apis/listCallerAccessGrants.ts @@ -1,15 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { AccessGrant, CredentialsProvider, ListLocationsOutput } from './types'; - -export interface ListCallerAccessGrantsInput { - accountId: string; - credentialsProvider: CredentialsProvider; - region: string; -} - -export type ListCallerAccessGrantsOutput = ListLocationsOutput; +import { + ListCallerAccessGrantsInput, + ListCallerAccessGrantsOutput, +} from './types'; export const listCallerAccessGrants = ( // eslint-disable-next-line unused-imports/no-unused-vars diff --git a/packages/storage/src/storageBrowser/apis/types.ts b/packages/storage/src/storageBrowser/apis/types.ts new file mode 100644 index 00000000000..2928cfd1f38 --- /dev/null +++ b/packages/storage/src/storageBrowser/apis/types.ts @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AccessGrant, + CredentialsProvider, + ListLocationsOutput, + LocationCredentials, + Permission, + PrefixType, + Privilege, +} from '../types'; + +export interface ListCallerAccessGrantsInput { + accountId: string; + credentialsProvider: CredentialsProvider; + region: string; +} + +export type ListCallerAccessGrantsOutput = ListLocationsOutput; + +export interface GetDataAccessInput { + accountId: string; + credentialsProvider: CredentialsProvider; + durationSeconds?: number; + permission: Permission; + prefixType?: PrefixType; + privilege?: Privilege; + region: string; + scope: string; +} + +export type GetDataAccessOutput = LocationCredentials; diff --git a/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createLocationCredentialsHandler.ts b/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createLocationCredentialsHandler.ts index 9cded212928..248e81882ac 100644 --- a/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createLocationCredentialsHandler.ts +++ b/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createLocationCredentialsHandler.ts @@ -1,7 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { CredentialsProvider, GetLocationCredentials } from '../types'; +import { getDataAccess } from '../apis/getDataAccess'; +import { + CredentialsProvider, + GetLocationCredentials, + GetLocationCredentialsInput, +} from '../types'; interface CreateLocationCredentialsHandlerInput { accountId: string; @@ -10,9 +15,26 @@ interface CreateLocationCredentialsHandlerInput { } export const createLocationCredentialsHandler = ( - // eslint-disable-next-line unused-imports/no-unused-vars - input: CreateLocationCredentialsHandlerInput, + handlerInput: CreateLocationCredentialsHandlerInput, ): GetLocationCredentials => { - // TODO(@AllanZhengYP) - throw new Error('Not Implemented'); + const { accountId, region, credentialsProvider } = handlerInput; + + /** + * Retrieves credentials for the specified scope & permission. + * + * @param input - An object specifying the requested scope & permission. + * + * @returns A promise which will resolve with the requested credentials. + */ + return (input: GetLocationCredentialsInput) => { + const { scope, permission } = input; + + return getDataAccess({ + accountId, + credentialsProvider, + permission, + region, + scope, + }); + }; }; diff --git a/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createManagedAuthConfigAdapter.ts b/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createManagedAuthConfigAdapter.ts index 55d334b47c7..267fee96c21 100644 --- a/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createManagedAuthConfigAdapter.ts +++ b/packages/storage/src/storageBrowser/managedAuthConfigAdapter/createManagedAuthConfigAdapter.ts @@ -38,6 +38,7 @@ export const createManagedAuthConfigAdapter = ({ accountId, region, }); + const getLocationCredentials = createLocationCredentialsHandler({ credentialsProvider, accountId, diff --git a/packages/storage/src/storageBrowser/types.ts b/packages/storage/src/storageBrowser/types.ts index 68fe71aadaf..c770b7472a3 100644 --- a/packages/storage/src/storageBrowser/types.ts +++ b/packages/storage/src/storageBrowser/types.ts @@ -22,7 +22,17 @@ export type CredentialsProvider = (options?: { */ export type LocationType = 'BUCKET' | 'PREFIX' | 'OBJECT'; -export interface CredentialsLocation { +/** + * @internal + */ +export type Privilege = 'Default' | 'Minimal'; + +/** + * @internal + */ +export type PrefixType = 'Object'; + +export interface LocationScope { /** * Scope of storage location. For S3 service, it's the S3 path of the data to * which the access is granted. It can be in following formats: @@ -32,6 +42,9 @@ export interface CredentialsLocation { * @example Object 's3:////' */ readonly scope: string; +} + +export interface CredentialsLocation extends LocationScope { /** * The type of access granted to your Storage data. Can be either of READ, * WRITE or READWRITE @@ -52,6 +65,13 @@ export interface LocationAccess extends CredentialsLocation { readonly type: LocationType; } +export interface LocationCredentials extends Partial { + /** + * AWS credentials which can be used to access the specified location. + */ + readonly credentials: AWSCredentials; +} + export interface AccessGrant extends LocationAccess { /** * The Amazon Resource Name (ARN) of an AWS IAM Identity Center application @@ -82,9 +102,12 @@ export type ListLocations = ( input?: ListLocationsInput, ) => Promise>; +export type GetLocationCredentialsInput = CredentialsLocation; +export type GetLocationCredentialsOutput = LocationCredentials; + export type GetLocationCredentials = ( - input: CredentialsLocation, -) => Promise<{ credentials: AWSCredentials }>; + input: GetLocationCredentialsInput, +) => Promise; export interface LocationCredentialsStore { /**