Updates to secrets manager; added foundation for e2e testing; began initialization for pipeline builds

This commit is contained in:
2023-04-29 00:58:15 -04:00
parent 29d36a57d6
commit bdfab94810
22 changed files with 9839 additions and 414 deletions

19
src/_jest_/setup.ts Normal file
View 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
View File

@@ -0,0 +1,8 @@
import { INestApplication } from '@nestjs/common';
const globalTeardown = async (_globalConfig, _projectConfig) => {
await (globalThis.__NESTAPP__ as INestApplication).close();
}
export default globalTeardown;

View File

@@ -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(),

View 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;
}

View 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,
}
}
}

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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}`;
}

View File

@@ -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,

View 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);
// });
});

View 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))
}
}
}

View File

@@ -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);
}
}

View File

@@ -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,