Updates to secrets manager; added foundation for e2e testing; began initialization for pipeline builds
This commit is contained in:
19
src/_jest_/setup.ts
Normal file
19
src/_jest_/setup.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppModule } from '../app.module';
|
||||
|
||||
const globalSetup = async (_globalConfig, _projectConfig) => {
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
const app: INestApplication = module.createNestApplication();
|
||||
await app.listen(4566);
|
||||
|
||||
globalThis.__TESTMODULE__ = module;
|
||||
globalThis.__NESTAPP__ = app;
|
||||
globalThis.__ENDPOINT__ = 'http://127.0.0.1:4566';
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
8
src/_jest_/teardown.ts
Normal file
8
src/_jest_/teardown.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
|
||||
const globalTeardown = async (_globalConfig, _projectConfig) => {
|
||||
|
||||
await (globalThis.__NESTAPP__ as INestApplication).close();
|
||||
}
|
||||
|
||||
export default globalTeardown;
|
||||
@@ -33,7 +33,6 @@ export class AppController {
|
||||
}, {})
|
||||
|
||||
const queryParams = { __path: request.path, ...body, ...lowerCasedHeaders };
|
||||
console.log({ queryParams })
|
||||
const actionKey = queryParams['x-amz-target'] ? 'x-amz-target' : 'Action';
|
||||
const { error: actionError } = Joi.object({
|
||||
[actionKey]: Joi.string().valid(...Object.values(Action)).required(),
|
||||
|
||||
22
src/config/omnibus.config.ts
Normal file
22
src/config/omnibus.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { CommonConfig } from "./common-config.interface";
|
||||
import { configValidator } from './config.validator';
|
||||
|
||||
export default (): CommonConfig => {
|
||||
|
||||
const { error, value } = configValidator.validate({
|
||||
AWS_ACCOUNT_ID: process.env.AWS_ACCOUNT_ID ?? '000000000000',
|
||||
AWS_REGION: process.env.AWS_REGION ?? 'us-east-1',
|
||||
DB_DATABASE: process.env.PERSISTANCE ?? ':memory:',
|
||||
DB_LOGGING: process.env.DEBUG ? true : false,
|
||||
DB_SYNCHRONIZE: true,
|
||||
HOST: process.env.HOST ?? 'localhost',
|
||||
PROTO: process.env.PROTOCOL ?? 'http',
|
||||
PORT: process.env.PORT as any ?? 4566,
|
||||
}, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
48
src/secrets-manager/delete-secret.handler.ts
Normal file
48
src/secrets-manager/delete-secret.handler.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { Secret } from './secret.entity';
|
||||
import { SecretService } from './secret.service';
|
||||
|
||||
type QueryParams = {
|
||||
SecretId: string;
|
||||
VersionId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DeleteSecretHandler extends AbstractActionHandler {
|
||||
|
||||
constructor(
|
||||
private readonly secretService: SecretService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Json;
|
||||
action = Action.SecretsManagerDeleteSecret;
|
||||
validator = Joi.object<QueryParams, true>({
|
||||
SecretId: Joi.string().required(),
|
||||
VersionId: Joi.string().allow(null, ''),
|
||||
});
|
||||
|
||||
protected async handle({ SecretId, VersionId}: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const name = Secret.getNameFromSecretId(SecretId);
|
||||
const secret = VersionId ?
|
||||
await this.secretService.findByNameAndVersion(name, VersionId) :
|
||||
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
||||
|
||||
if (!secret) {
|
||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||
}
|
||||
|
||||
secret.deletionDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 5).toISOString();
|
||||
await secret.save();
|
||||
return {
|
||||
Arn: secret.arn,
|
||||
DeletionDate: secret.deletionDate,
|
||||
Name: secret.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class DescribeSecretHandler extends AbstractActionHandler {
|
||||
return {
|
||||
"ARN": secret.arn,
|
||||
"CreatedDate": new Date(secret.createdAt).toISOString(),
|
||||
"DeletedDate": null,
|
||||
"DeletedDate": secret.deletionDate ? new Date(secret.deletionDate).toISOString() : null,
|
||||
"Description": secret.description,
|
||||
"KmsKeyId": "",
|
||||
"LastChangedDate": new Date(secret.createdAt).toISOString(),
|
||||
|
||||
@@ -39,7 +39,7 @@ export class GetSecretValueHandler extends AbstractActionHandler {
|
||||
|
||||
return {
|
||||
ARN: secret.arn,
|
||||
CreatedDate: new Date(secret.createdAt).valueOf(),
|
||||
CreatedDate: new Date(secret.createdAt).valueOf() / 1000,
|
||||
Name: secret.name,
|
||||
SecretString: secret.secretString,
|
||||
VersionId: secret.versionId,
|
||||
|
||||
@@ -25,6 +25,9 @@ export class Secret extends BaseEntity {
|
||||
@CreateDateColumn()
|
||||
createdAt: string;
|
||||
|
||||
@Column({ name: 'deletion_date', nullable: true })
|
||||
deletionDate: string;
|
||||
|
||||
get arn(): string {
|
||||
return `arn:aws:secretsmanager:${this.region}:${this.accountId}:${this.name}`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entit
|
||||
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||
import { CreateSecretHandler } from './create-secret.handler';
|
||||
import { DeleteSecretHandler } from './delete-secret.handler';
|
||||
import { DescribeSecretHandler } from './describe-secret.handler';
|
||||
import { GetResourcePolicyHandler } from './get-resource-policy.handler';
|
||||
import { GetSecretValueHandler } from './get-secret-value.handler';
|
||||
@@ -17,6 +18,7 @@ import { SecretsManagerHandlers } from './secrets-manager.constants';
|
||||
|
||||
const handlers = [
|
||||
CreateSecretHandler,
|
||||
DeleteSecretHandler,
|
||||
DescribeSecretHandler,
|
||||
GetResourcePolicyHandler,
|
||||
GetSecretValueHandler,
|
||||
|
||||
165
src/sns/__tests__/sns.module.spec.ts
Normal file
165
src/sns/__tests__/sns.module.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { CreateTopicCommand, CreateTopicCommandOutput, GetSubscriptionAttributesCommand, GetTopicAttributesCommand, ListTagsForResourceCommand, ListTopicsCommand, PublishCommand, SNSClient, SubscribeCommand, SubscribeCommandOutput } from '@aws-sdk/client-sns';
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Tag } from '../../aws-shared-entities/tags.entity';
|
||||
import { SnsTopicSubscription } from '../sns-topic-subscription.entity';
|
||||
import { SnsTopic } from '../sns-topic.entity';
|
||||
|
||||
describe('SNS Module', () => {
|
||||
|
||||
let snsClient: SNSClient;
|
||||
|
||||
beforeAll(async () => {
|
||||
snsClient = new SNSClient({
|
||||
endpoint: globalThis.__ENDPOINT__,
|
||||
});
|
||||
});
|
||||
|
||||
const describeCleanup = async () => {
|
||||
const testModule: TestingModule = globalThis.__TESTMODULE__;
|
||||
|
||||
const snsTopicRepo = testModule.get<Repository<SnsTopic>>(getRepositoryToken(SnsTopic));
|
||||
await snsTopicRepo.delete({});
|
||||
|
||||
const subscriptionRepo = testModule.get<Repository<SnsTopicSubscription>>(getRepositoryToken(SnsTopicSubscription));
|
||||
await subscriptionRepo.delete({});
|
||||
|
||||
const tagsRepo = testModule.get<Repository<Tag>>(getRepositoryToken(Tag));
|
||||
await tagsRepo.delete({});
|
||||
};
|
||||
|
||||
describe('creation', () => {
|
||||
|
||||
afterAll(describeCleanup);
|
||||
|
||||
it('can create a topic', async () => {
|
||||
const response = await snsClient.send(new CreateTopicCommand({
|
||||
Name: 'test-topic-1',
|
||||
}));
|
||||
|
||||
expect(response.TopicArn).toBe('arn:aws:sns:us-east-1:000000000000:test-topic-1');
|
||||
});
|
||||
|
||||
it('can subscribe', async () => {
|
||||
|
||||
const topicResponse = await snsClient.send(new CreateTopicCommand({
|
||||
Name: 'test-topic-2',
|
||||
}));
|
||||
|
||||
const response = await snsClient.send(new SubscribeCommand({
|
||||
TopicArn: topicResponse.TopicArn,
|
||||
Protocol: 'https',
|
||||
Endpoint: 'https://google.com',
|
||||
}));
|
||||
|
||||
expect(response.SubscriptionArn).toBeDefined();
|
||||
});
|
||||
|
||||
it('can publish', async () => {
|
||||
|
||||
const topicResponse = await snsClient.send(new CreateTopicCommand({
|
||||
Name: 'test-topic-3',
|
||||
}));
|
||||
|
||||
const response = await snsClient.send(new PublishCommand({
|
||||
Message: "hello world",
|
||||
TopicArn: topicResponse.TopicArn,
|
||||
}));
|
||||
|
||||
expect(response.MessageId).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reading', () => {
|
||||
|
||||
afterAll(describeCleanup);
|
||||
|
||||
let subscribedTopic: CreateTopicCommandOutput;
|
||||
let subscription: SubscribeCommandOutput;
|
||||
|
||||
beforeAll(async () => {
|
||||
subscribedTopic = await snsClient.send(new CreateTopicCommand({
|
||||
Name: 'test-topic-4',
|
||||
Tags: [{ Key: 'V_a', Value: 'a' }, { Key: 'V_b', Value: 'b', }]
|
||||
}));
|
||||
|
||||
await snsClient.send(new CreateTopicCommand({
|
||||
Name: 'test-topic-5',
|
||||
}));
|
||||
|
||||
await snsClient.send(new CreateTopicCommand({
|
||||
Name: 'test-topic-6',
|
||||
}));
|
||||
|
||||
subscription = await snsClient.send(new SubscribeCommand({
|
||||
TopicArn: subscribedTopic.TopicArn,
|
||||
Protocol: 'https',
|
||||
Endpoint: 'https://google.com',
|
||||
}));
|
||||
});
|
||||
|
||||
it('can get subscription attributes', async () => {
|
||||
const response = await snsClient.send(new GetSubscriptionAttributesCommand({
|
||||
SubscriptionArn: subscription.SubscriptionArn,
|
||||
}));
|
||||
|
||||
expect(response.Attributes).toEqual(expect.objectContaining({
|
||||
"ConfirmationWasAuthenticated": "true",
|
||||
"PendingConfirmation": "false",
|
||||
"Owner": "000000000000",
|
||||
"SubscriptionArn": subscription.SubscriptionArn,
|
||||
"TopicArn": subscribedTopic.TopicArn,
|
||||
"TracingConfig": "PassThrough"
|
||||
}));
|
||||
});
|
||||
|
||||
it('can get topic attributes', async () => {
|
||||
|
||||
const response = await snsClient.send(new GetTopicAttributesCommand({
|
||||
TopicArn: subscribedTopic.TopicArn,
|
||||
}));
|
||||
|
||||
expect(response.Attributes).toEqual(expect.objectContaining({
|
||||
"DisplayName": 'test-topic-4',
|
||||
"Owner": "000000000000",
|
||||
"SubscriptionsConfirmed": "1",
|
||||
"SubscriptionsDeleted": "0",
|
||||
"SubscriptionsPending": "0",
|
||||
"TopicArn": subscribedTopic.TopicArn,
|
||||
"TracingConfig": "PassThrough"
|
||||
}));
|
||||
});
|
||||
|
||||
it('can list tags for resource', async () => {
|
||||
|
||||
const response = await snsClient.send(new ListTagsForResourceCommand({
|
||||
ResourceArn: subscribedTopic.TopicArn,
|
||||
}));
|
||||
|
||||
expect(response.Tags).toHaveLength(2);
|
||||
|
||||
const map = new Map(response.Tags.map(({ Key, Value }) => [ Key, Value ]));
|
||||
expect(map.get('V_a')).toBe('a');
|
||||
expect(map.get('V_b')).toBe('b');
|
||||
});
|
||||
|
||||
it('can list all topics', async () => {
|
||||
const response = await snsClient.send(new ListTopicsCommand({}));
|
||||
expect(response.Topics).toHaveLength(3);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// describe('updating', () => {
|
||||
|
||||
// afterAll(describeCleanup);
|
||||
|
||||
// });
|
||||
|
||||
// describe('deleting', () => {
|
||||
|
||||
// afterAll(describeCleanup);
|
||||
|
||||
// });
|
||||
});
|
||||
34
src/sqs/list-queues.handler.ts
Normal file
34
src/sqs/list-queues.handler.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||
import { Action } from '../action.enum';
|
||||
import * as Joi from 'joi';
|
||||
import { SqsQueue } from './sqs-queue.entity';
|
||||
|
||||
type QueryParams = {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ListQueuesHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SqsQueue)
|
||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
format = Format.Xml;
|
||||
action = Action.SqsListQueues;
|
||||
validator = Joi.object<QueryParams, true>();
|
||||
|
||||
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const queues = await this.sqsQueueRepo.find({ where: { accountId: awsProperties.accountId }});
|
||||
|
||||
return {
|
||||
QueueUrl: queues.map((q) => q.getUrl(awsProperties.host))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { SqsQueue } from './sqs-queue.entity';
|
||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||
|
||||
type QueryParams = {
|
||||
__path: string;
|
||||
QueueUrl: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -20,11 +20,11 @@ export class PurgeQueueHandler extends AbstractActionHandler<QueryParams> {
|
||||
|
||||
format = Format.Xml;
|
||||
action = Action.SqsPurgeQueue;
|
||||
validator = Joi.object<QueryParams, true>({ __path: Joi.string().required() });
|
||||
validator = Joi.object<QueryParams, true>({ QueueUrl: Joi.string().required() });
|
||||
|
||||
protected async handle({ __path }: QueryParams, awsProperties: AwsProperties) {
|
||||
protected async handle({ QueueUrl }: QueryParams, awsProperties: AwsProperties) {
|
||||
|
||||
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(__path);
|
||||
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
|
||||
await this.sqsQueueEntryService.purge(accountId, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ExistingActionHandlersProvider } from '../default-action-handler/existi
|
||||
import { CreateQueueHandler } from './create-queue.handler';
|
||||
import { DeleteQueueHandler } from './delete-queue.handler';
|
||||
import { GetQueueAttributesHandler } from './get-queue-attributes.handler';
|
||||
import { ListQueuesHandler } from './list-queues.handler';
|
||||
import { PurgeQueueHandler } from './purge-queue.handler';
|
||||
import { ReceiveMessageHandler } from './receive-message.handler';
|
||||
import { SetQueueAttributesHandler } from './set-queue-attributes.handler';
|
||||
@@ -19,6 +20,7 @@ const handlers = [
|
||||
CreateQueueHandler,
|
||||
DeleteQueueHandler,
|
||||
GetQueueAttributesHandler,
|
||||
ListQueuesHandler,
|
||||
PurgeQueueHandler,
|
||||
ReceiveMessageHandler,
|
||||
SetQueueAttributesHandler,
|
||||
|
||||
Reference in New Issue
Block a user