Skip to content

Commit

Permalink
feat(fs): support caching of aws credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
blacha committed Sep 16, 2021
1 parent 9286289 commit c0b08b3
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 38 deletions.
2 changes: 1 addition & 1 deletion packages/fs/src/fs.abstraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class FileSystemAbstraction {
* @see FileSystemAbstraction.sortSystems
*/
private isOrdered = true;
private systems: { path: string; system: FileSystem }[] = [];
systems: { path: string; system: FileSystem }[] = [];

/**
* Register a file system to a specific path which can then be used with any `fsa` command
Expand Down
2 changes: 1 addition & 1 deletion packages/fs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { FileSystemAbstraction, fsa } from './fs.abstraction.js';
export { FsAwsS3 } from '@chunkd/source-aws';
export { FsAwsS3, AwsCredentials } from '@chunkd/source-aws';
export { FsHttp } from '@chunkd/source-http';
32 changes: 32 additions & 0 deletions packages/source-aws/src/__test__/s3.credentials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import o from 'ospec';
import Sinon from 'sinon';
import aws from 'aws-sdk/lib/core.js';
import { AwsCredentials } from '../s3.credentials.js';

o.spec('AwsCredentials', () => {
const sandbox = Sinon.createSandbox();

o.beforeEach(() => sandbox.restore());
o('should default to 3600 second duration', () => {
const stub = sandbox.stub(aws, 'ChainableTemporaryCredentials');
AwsCredentials.role('foo');
o(stub.args[0][0].params.DurationSeconds).equals(3600);
AwsCredentials.role('foo', undefined, 3600);
o(stub.calledOnce).equals(true);

AwsCredentials.role('foo', undefined, 3601);
o(stub.callCount).equals(2);
o(stub.args[1][0].params.DurationSeconds).equals(3601);
});

o('should cache by roleArn', () => {
const stub = sandbox.stub(aws, 'ChainableTemporaryCredentials');
AwsCredentials.role('arn:foo:bar');
AwsCredentials.role('arn:foo:bar');
o(stub.calledOnce).equals(true);
AwsCredentials.role('arn:foo:baz');
o(stub.callCount).equals(2);
AwsCredentials.role('arn:foo:baz', 'external');
o(stub.callCount).equals(3);
});
});
1 change: 1 addition & 0 deletions packages/source-aws/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { SourceAwsS3 } from './s3.source.js';
export { FsAwsS3 } from './s3.fs.js';
export { AwsCredentials } from './s3.credentials.js';
26 changes: 26 additions & 0 deletions packages/source-aws/src/s3.credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Credentials } from 'aws-sdk/lib/credentials.js';
import aws from 'aws-sdk/lib/core.js';

export class AwsCredentials {
static defaultRoleDuration = 3600;
static cache: Map<string, Credentials> = new Map();

static role(roleArn: string, externalId?: string, duration?: number): Credentials {
duration = duration ?? AwsCredentials.defaultRoleDuration;

const roleKey = `role::${roleArn}::${externalId}::${duration}`;
let existing = AwsCredentials.cache.get(roleKey);
if (existing == null) {
existing = new aws.ChainableTemporaryCredentials({
params: {
RoleArn: roleArn,
ExternalId: externalId,
RoleSessionName: 'fsa-' + Math.random().toString(32) + '-' + Date.now(),
DurationSeconds: duration,
},
});
AwsCredentials.cache.set(roleKey, existing);
}
return existing;
}
}
43 changes: 7 additions & 36 deletions packages/source-aws/src/s3.fs.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,28 @@
import { FileInfo, FileSystem, isRecord, WriteOptions } from '@chunkd/core';
import S3 from 'aws-sdk/clients/s3.js';
import aws from 'aws-sdk/lib/core.js';
import { Credentials } from 'aws-sdk/lib/credentials.js';
import type { Readable } from 'stream';
import { AwsCredentials } from './s3.credentials.js';
import { getCompositeError, SourceAwsS3 } from './s3.source.js';
import { ListRes, S3Like } from './type.js';

const Ec2 = Symbol('ec2');

export class FsAwsS3 implements FileSystem<SourceAwsS3> {
static protocol = 's3';
protocol = FsAwsS3.protocol;
/** Max list requests to run before erroring */
static MaxListCount = 100;

static Ec2Credentials = Ec2;

static credentials: Map<string, Credentials> = new Map();
/**
* Create a aws credential instance from a role arn
*
* if the AWS profile is "FsAwsS3.Ec2Credentials" use EC2MetadataCredentials, otherwise load credentials from the shared ini file
*/
static getCredentials(roleArn: string, profile?: string | typeof Ec2, externalId?: string): Credentials {
const credKey = `${roleArn}::${roleArn}::${externalId}`;
let credentials = FsAwsS3.credentials.get(credKey);
if (credentials == null) {
const masterCredentials =
profile === Ec2 ? new aws.EC2MetadataCredentials() : new aws.SharedIniFileCredentials({ profile });
credentials = new aws.ChainableTemporaryCredentials({
params: {
RoleArn: roleArn,
ExternalId: externalId,
RoleSessionName: 'fsa-' + Math.random().toString(32) + '-' + Date.now(),
},
masterCredentials,
});
FsAwsS3.credentials.set(credKey, credentials);
}
return credentials;
}
/** Credential cache to allow reuse of credentials */
static credentials = AwsCredentials;

/**
* Create a FsS3 instance from a role arn
*
* if the AWS profile is "ec2" use EC2MetadataCredentials, otherwise load credentials from the shared ini file
*
* @example
* Fs3.fromRoleArn('arn:foo', 'ec2');
* FsS3.fromRoleArn('arn:bar', process.env.AWS_PROFILE);
* Fs3.fromRoleArn('arn:foo', externalId, 900);
* FsS3.fromRoleArn('arn:bar');
*/
static fromRoleArn(roleArn: string, profile?: string | typeof Ec2, externalId?: string): FsAwsS3 {
const credentials = FsAwsS3.getCredentials(roleArn, profile, externalId);
static fromRoleArn(roleArn: string, externalId?: string, duration?: number): FsAwsS3 {
const credentials = FsAwsS3.credentials.role(roleArn, externalId, duration);
return new FsAwsS3(new S3({ credentials }));
}

Expand Down

0 comments on commit c0b08b3

Please sign in to comment.