Skip to content

Commit

Permalink
feat: Implement getLocationCredentials handler & integrate with adapt…
Browse files Browse the repository at this point in the history
…er (#13600)
  • Loading branch information
jimblanc authored Jul 17, 2024
1 parent 06c093b commit e44d290
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 23 deletions.
12 changes: 6 additions & 6 deletions packages/aws-amplify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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",
Expand Down Expand Up @@ -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)",
Expand All @@ -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)",
Expand Down Expand Up @@ -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)",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/Platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export enum StorageAction {
Remove = '5',
GetProperties = '6',
GetUrl = '7',
GetDataAccess = '8',
}

interface ActionMap {
Expand Down
116 changes: 116 additions & 0 deletions packages/storage/__tests__/storageBrowser/apis/getDataAccess.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
4 changes: 4 additions & 0 deletions packages/storage/src/storageBrowser/apis/constants.ts
Original file line number Diff line number Diff line change
@@ -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
66 changes: 66 additions & 0 deletions packages/storage/src/storageBrowser/apis/getDataAccess.ts
Original file line number Diff line number Diff line change
@@ -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<GetDataAccessOutput> => {
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,
};
};
Original file line number Diff line number Diff line change
@@ -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<AccessGrant>;
import {
ListCallerAccessGrantsInput,
ListCallerAccessGrantsOutput,
} from './types';

export const listCallerAccessGrants = (
// eslint-disable-next-line unused-imports/no-unused-vars
Expand Down
33 changes: 33 additions & 0 deletions packages/storage/src/storageBrowser/apis/types.ts
Original file line number Diff line number Diff line change
@@ -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<AccessGrant>;

export interface GetDataAccessInput {
accountId: string;
credentialsProvider: CredentialsProvider;
durationSeconds?: number;
permission: Permission;
prefixType?: PrefixType;
privilege?: Privilege;
region: string;
scope: string;
}

export type GetDataAccessOutput = LocationCredentials;
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
});
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const createManagedAuthConfigAdapter = ({
accountId,
region,
});

const getLocationCredentials = createLocationCredentialsHandler({
credentialsProvider,
accountId,
Expand Down
Loading

0 comments on commit e44d290

Please sign in to comment.