Initial commit

This commit is contained in:
Matthew Bessette 2023-03-21 00:12:10 -04:00
commit bb911f8ffb
41 changed files with 4390 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
./local-aws.sqlite

5
nest-cli.json Normal file
View File

@ -0,0 +1,5 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"entryFile": "main.js"
}

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "local-aws",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "nest start",
"start:dev": "nest start --watch"
},
"dependencies": {
"@nestjs/common": "^9.3.10",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.3.10",
"@nestjs/platform-express": "^9.3.10",
"@nestjs/typeorm": "^9.0.1",
"class-transformer": "^0.5.1",
"joi": "^17.9.0",
"js2xmlparser": "^5.0.0",
"morgan": "^1.10.0",
"rxjs": "^7.8.0",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.12",
"uuidv4": "^6.2.13"
},
"devDependencies": {
"@nestjs/cli": "^9.3.0"
}
}

View File

@ -0,0 +1,59 @@
import { Action } from './action.enum';
import * as uuid from 'uuid';
import * as Joi from 'joi';
export type AwsProperties = {
accountId: string;
region: string;
}
export enum Format {
Xml,
Json,
};
export abstract class AbstractActionHandler<T = Record<string, string | number | boolean>> {
abstract format: Format;
abstract action: Action;
abstract validator: Joi.ObjectSchema<T>;
protected abstract handle(queryParams: T, awsProperties: AwsProperties): Record<string, any> | void;
async getResponse(queryParams: T, awsProperties: AwsProperties) {
if (this.format === Format.Xml) {
return await this.getXmlResponse(queryParams, awsProperties);
}
return await this.getJsonResponse(queryParams, awsProperties);
}
private async getXmlResponse(queryParams: T, awsProperties: AwsProperties) {
const response = {
'@': {
xmlns: "https://sns.amazonaws.com/doc/2010-03-31/"
},
ResponseMetadata: {
RequestId: uuid.v4(),
}
}
const result = await this.handle(queryParams, awsProperties);
if (!result) {
return response;
}
return {
[`${this.action}Result`]: {
...result,
}
}
}
private async getJsonResponse(queryParams: T, awsProperties: AwsProperties) {
const result = await this.handle(queryParams, awsProperties);
if (result) {
return result;
}
return;
}
}

92
src/action.enum.ts Normal file
View File

@ -0,0 +1,92 @@
export enum Action {
// SecretsManager
SecretsManagerCancelRotateSecret = 'secretsmanager.CancelRotateSecret',
SecretsManagerCreateSecret = 'secretsmanager.CreateSecret',
SecretsManagerDeleteResourcePolicy = 'secretsmanager.DeleteResourcePolicy',
SecretsManagerDeleteSecret = 'secretsmanager.DeleteSecret',
SecretsManagerDescribeSecret = 'secretsmanager.DescribeSecret',
SecretsManagerGetRandomPassword = 'secretsmanager.GetRandomPassword',
SecretsManagerGetResourcePolicy = 'secretsmanager.GetResourcePolicy',
SecretsManagerGetSecretValue = 'secretsmanager.GetSecretValue',
SecretsManagerListSecrets = 'secretsmanager.ListSecrets',
SecretsManagerListSecretVersionIds = 'secretsmanager.ListSecretVersionIds',
SecretsManagerPutResourcePolicy = 'secretsmanager.PutResourcePolicy',
SecretsManagerPutSecretValue = 'secretsmanager.PutSecretValue',
SecretsManagerRemoveRegionsFromReplication = 'secretsmanager.RemoveRegionsFromReplication',
SecretsManagerReplicateSecretToRegions = 'secretsmanager.ReplicateSecretToRegions',
SecretsManagerRestoreSecret = 'secretsmanager.RestoreSecret',
SecretsManagerRotateSecret = 'secretsmanager.RotateSecret',
SecretsManagerStopReplicationToReplica = 'secretsmanager.StopReplicationToReplica',
SecretsManagerTagResource = 'secretsmanager.TagResource',
SecretsManagerUntagResource = 'secretsmanager.UntagResource',
SecretsManagerUpdateSecret = 'secretsmanager.UpdateSecret',
SecretsManagerUpdateSecretVersionStage = 'secretsmanager.UpdateSecretVersionStage',
SecretsManagerValidateResourcePolicy = 'secretsmanager.ValidateResourcePolicy',
// SNS
SnsAddPermission = 'AddPermission',
SnsCheckIfPhoneNumberIsOptedOut = 'CheckIfPhoneNumberIsOptedOut',
SnsConfirmSubscription = 'ConfirmSubscription',
SnsCreatePlatformApplication = 'CreatePlatformApplication',
SnsCreatePlatformEndpoint = 'CreatePlatformEndpoint',
SnsCreateSMSSandboxPhoneNumber = 'CreateSMSSandboxPhoneNumber',
SnsCreateTopic = 'CreateTopic',
SnsDeleteEndpoint = 'DeleteEndpoint',
SnsDeletePlatformApplication = 'DeletePlatformApplication',
SnsDeleteSMSSandboxPhoneNumber = 'DeleteSMSSandboxPhoneNumber',
SnsDeleteTopic = 'DeleteTopic',
SnsGetDataProtectionPolicy = 'GetDataProtectionPolicy',
SnsGetEndpointAttributes = 'GetEndpointAttributes',
SnsGetPlatformApplicationAttributes = 'GetPlatformApplicationAttributes',
SnsGetSMSAttributes = 'GetSMSAttributes',
SnsGetSMSSandboxAccountStatus = 'GetSMSSandboxAccountStatus',
SnsGetSubscriptionAttributes = 'GetSubscriptionAttributes',
SnsGetTopicAttributes = 'GetTopicAttributes',
SnsListEndpointsByPlatformApplication = 'ListEndpointsByPlatformApplication',
SnsListOriginationNumbers = 'ListOriginationNumbers',
SnsListPhoneNumbersOptedOut = 'ListPhoneNumbersOptedOut',
SnsListPlatformApplications = 'ListPlatformApplications',
SnsListSMSSandboxPhoneNumbers = 'ListSMSSandboxPhoneNumbers',
SnsListSubscriptions = 'ListSubscriptions',
SnsListSubscriptionsByTopic = 'ListSubscriptionsByTopic',
SnsListTagsForResource = 'ListTagsForResource',
SnsListTopics = 'ListTopics',
SnsOptInPhoneNumber = 'OptInPhoneNumber',
SnsPublish = 'Publish',
SnsPublishBatch = 'PublishBatch',
SnsPutDataProtectionPolicy = 'PutDataProtectionPolicy',
SnsRemovePermission = 'RemovePermission',
SnsSetEndpointAttributes = 'SetEndpointAttributes',
SnsSetPlatformApplicationAttributes = 'SetPlatformApplicationAttributes',
SnsSetSMSAttributes = 'SetSMSAttributes',
SnsSetSubscriptionAttributes = 'SetSubscriptionAttributes',
SnsSetTopicAttributes = 'SetTopicAttributes',
SnsSubscribe = 'Subscribe',
SnsTagResource = 'TagResource',
SnsUnsubscribe = 'Unsubscribe',
SnsUntagResource = 'UntagResource',
SnsVerifySMSSandboxPhoneNumber = 'VerifySMSSandboxPhoneNumber',
// SQS
SqsAddPermisson = 'AddPermission',
SqsChangeMessageVisibility = 'ChangeMessageVisibility',
SqsChangeMessageVisibilityBatch = 'ChangeMessageVisibilityBatch',
SqsCreateQueue = 'CreateQueue',
SqsDeleteMessage = 'DeleteMessage',
SqsDeleteMessageBatch = 'DeleteMessageBatch',
SqsDeleteQueue = 'DeleteQueue',
SqsGetQueueAttributes = 'GetQueueAttributes',
SqsGetQueueUrl = 'GetQueueUrl',
SqsListDeadLetterSourceQueues = 'ListDeadLetterSourceQueues',
SqsListQueues = 'ListQueues',
SqsListQueueTags = 'ListQueueTags',
SqsPurgeQueue = 'PurgeQueue',
SqsReceiveMessage = 'ReceiveMessage',
SqsRemovePermission = 'RemovePermission',
SqsSendMessage = 'SendMessage',
SqsSendMessageBatch = 'SendMessageBatch',
SqsSetQueueAttributes = 'SetQueueAttributes',
SqsTagQueue = 'TagQueue',
SqsUntagQueue = 'UntagQueue',
}

5
src/app.constants.ts Normal file
View File

@ -0,0 +1,5 @@
import { AbstractActionHandler } from './abstract-action.handler';
import { Action } from './action.enum';
export type ActionHandlers = Record<Action, AbstractActionHandler>;
export const ActionHandlers = Symbol.for('ACTION_HANDLERS');

61
src/app.controller.ts Normal file
View File

@ -0,0 +1,61 @@
import { BadRequestException, Body, Controller, Get, Inject, Post, Headers } from '@nestjs/common';
import { ActionHandlers } from './app.constants';
import * as Joi from 'joi';
import { Action } from './action.enum';
import { AbstractActionHandler, Format } from './abstract-action.handler';
import * as js2xmlparser from 'js2xmlparser';
import { ConfigService } from '@nestjs/config';
import { CommonConfig } from './config/common-config.interface';
@Controller()
export class AppController {
constructor(
@Inject(ActionHandlers)
private readonly actionHandlers: ActionHandlers,
private readonly configService: ConfigService<CommonConfig>,
) {}
@Post()
async post(
@Body() body: Record<string, any>,
@Headers() headers: Record<string, any>,
) {
const lowerCasedHeaders = Object.keys(headers).reduce((o, k) => {
o[k.toLocaleLowerCase()] = headers[k];
return o;
}, {})
const queryParams = { ...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(),
}).validate(queryParams, { allowUnknown: true });
if (actionError) {
throw new BadRequestException(actionError);
}
const action = queryParams[actionKey];
const handler: AbstractActionHandler = this.actionHandlers[action];
const { error: validatorError, value: validQueryParams } = handler.validator.validate(queryParams, { allowUnknown: true, abortEarly: false });
if (validatorError) {
throw new BadRequestException(validatorError);
}
const awsProperties = { accountId: this.configService.get('AWS_ACCOUNT_ID'), region: this.configService.get('AWS_REGION') };
const jsonResponse = await handler.getResponse(validQueryParams, awsProperties);
if (handler.format === Format.Xml) {
const xmlResponse = js2xmlparser.parse(`${handler.action}Response`, jsonResponse);
// console.log({xmlResponse})
return xmlResponse;
}
return jsonResponse;
}
}

49
src/app.module.ts Normal file
View File

@ -0,0 +1,49 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ActionHandlers } from './app.constants';
import { CommonConfig } from './config/common-config.interface';
import localConfig from './config/local.config';
import { SnsHandlers } from './sns/sns.constants';
import { SnsModule } from './sns/sns.module';
import { AppController } from './app.controller';
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
import { SecretsManagerModule } from './secrets-manager/secrets-manager.module';
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
@Module({
imports: [
ConfigModule.forRoot({
load: [localConfig],
isGlobal: true,
// validationSchema: configValidator,
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService<CommonConfig>) => ({
type: 'sqlite',
database: configService.get('DB_DATABASE'),
logging: configService.get('DB_LOGGING'),
synchronize: configService.get('DB_SYNCHRONIZE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
}),
}),
SnsModule,
SecretsManagerModule,
AwsSharedEntitiesModule,
],
controllers: [
AppController,
],
providers: [
{
provide: ActionHandlers,
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
inject: [
SnsHandlers,
SecretsManagerHandlers,
],
},
],
})
export class AppModule {}

View File

@ -0,0 +1,18 @@
import { BaseEntity, Column, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
@Entity('attributes')
export class Attribute extends BaseEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: string;
@Column({ name: 'arn', nullable: false })
@Index()
arn: string;
@Column({ name: 'name', nullable: false })
name: string;
@Column({ name: 'value', nullable: false })
value: string;
}

View File

@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Attribute } from './attributes.entity';
import { CreateAttributeDto } from './create-attribute.dto';
@Injectable()
export class AttributesService {
constructor(
@InjectRepository(Attribute)
private readonly repo: Repository<Attribute>,
) {}
async getByArn(arn: string): Promise<Attribute[]> {
return await this.repo.find({ where: { arn }});
}
async create(dto: CreateAttributeDto): Promise<Attribute> {
return await this.repo.save(dto);
}
async deleteByArnAndName(arn: string, name: string) {
await this.repo.delete({ arn, name });
}
async createMany(arn: string, records: { key: string, value: string }[]): Promise<void> {
for (const record of records) {
await this.create({ arn, name: record.key, value: record.value });
}
}
static attributePairs(queryParams: Record<string, string>): { key: string, value: string }[] {
const pairs = [null];
for (const param of Object.keys(queryParams)) {
const [type, _, idx, slot] = param.split('.');
if (type === 'Attributes') {
if (!pairs[+idx]) {
pairs[+idx] = { key: '', value: ''};
}
pairs[+idx][slot] = queryParams[param];
}
}
pairs.shift();
return pairs;
}
static getXmlSafeAttributesMap(attributes: Record<string, string>) {
return { Attributes: { entry: Object.keys(attributes).map(key => ({ key, value: attributes[key] })) } }
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Attribute } from './attributes.entity';
import { AttributesService } from './attributes.service';
import { Tag } from './tags.entity';
import { TagsService } from './tags.service';
@Module({
imports: [TypeOrmModule.forFeature([Attribute, Tag])],
providers: [AttributesService, TagsService],
exports: [AttributesService, TagsService],
})
export class AwsSharedEntitiesModule {}

View File

@ -0,0 +1,5 @@
export interface CreateAttributeDto {
arn: string;
name: string;
value: string;
}

View File

@ -0,0 +1,5 @@
export interface CreateTagDto {
arn: string;
name: string;
value: string;
}

View File

@ -0,0 +1,18 @@
import { BaseEntity, Column, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
@Entity('tags')
export class Tag extends BaseEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: string;
@Column({ name: 'arn', nullable: false})
@Index()
arn: string;
@Column({ name: 'name', nullable: false })
name: string;
@Column({ name: 'value', nullable: false })
value: string;
}

View File

@ -0,0 +1,53 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tag } from './tags.entity';
import { CreateTagDto } from './create-tag.dto';
@Injectable()
export class TagsService {
constructor(
@InjectRepository(Tag)
private readonly repo: Repository<Tag>,
) {}
async getByArn(arn: string): Promise<Tag[]> {
return await this.repo.find({ where: { arn }});
}
async create(dto: CreateTagDto): Promise<Tag> {
return await this.repo.save(dto);
}
async createMany(arn: string, records: { Key: string, Value: string }[]): Promise<void> {
for (const record of records) {
await this.create({ arn, name: record.Key, value: record.Value });
}
}
async deleteByArnAndName(arn: string, name: string) {
await this.repo.delete({ arn, name });
}
static tagPairs(queryParams: Record<string, string>): { Key: string, Value: string }[] {
const pairs = [null];
for (const param of Object.keys(queryParams)) {
const [type, _, idx, slot] = param.split('.');
if (type === 'Tags') {
if (!pairs[+idx]) {
pairs[+idx] = { Key: '', Value: ''};
}
pairs[+idx][slot] = queryParams[param];
}
}
pairs.shift();
return pairs;
}
static getXmlSafeAttributesMap(tags: Tag[]) {
return { Tags: { member: tags.map(({ name, value }) => ({ Key: name, Value: value })) } };
}
}

View File

@ -0,0 +1,7 @@
export interface CommonConfig {
AWS_ACCOUNT_ID: string;
AWS_REGION: string;
DB_DATABASE: string;
DB_LOGGING?: boolean;
DB_SYNCHRONIZE?: boolean;
}

View File

@ -0,0 +1,9 @@
import { CommonConfig } from "./common-config.interface";
export default (): CommonConfig => ({
AWS_ACCOUNT_ID: '123456789012',
AWS_REGION: 'us-east-1',
DB_DATABASE: ':memory:', // 'local-aws.sqlite', // :memory:
DB_LOGGING: true,
DB_SYNCHRONIZE: true,
});

View File

@ -0,0 +1,5 @@
import { AbstractActionHandler } from '../abstract-action.handler';
import { Action } from '../action.enum';
export type ExistingActionHandlers = Record<Action, AbstractActionHandler>;
export const ExistingActionHandlers = Symbol.for('EXISTING_ACTION_HANDLERS');

View File

@ -0,0 +1,19 @@
import { Provider } from '@nestjs/common';
import { Action } from '../action.enum';
import { ExistingActionHandlers } from './default-action-handler.constants';
import * as Joi from 'joi';
import { AbstractActionHandler, Format } from '../abstract-action.handler';
export const DefaultActionHandlerProvider = (symbol, format: Format, actions: Action[]): Provider => ({
provide: symbol,
useFactory: (existingActionHandlers: ExistingActionHandlers) => {
const cloned = { ...existingActionHandlers };
for (const action of actions) {
if (!cloned[action]) {
cloned[action] = new (class Default extends AbstractActionHandler { action = action; format = format; validator = Joi.object(); handle = () => {} });
}
}
return cloned;
},
inject: [ExistingActionHandlers]
});

View File

@ -0,0 +1,12 @@
import { Provider } from '@nestjs/common';
import { AbstractActionHandler } from '../abstract-action.handler';
import { ExistingActionHandlers } from './default-action-handler.constants';
export const ExistingActionHandlersProvider = (inject): Provider => ({
provide: ExistingActionHandlers,
useFactory: (...args: AbstractActionHandler[]) => args.reduce((m, h) => {
m[h.action] = h;
return m;
}, {}),
inject,
});

4
src/group.enum.ts Normal file
View File

@ -0,0 +1,4 @@
export enum Group {
Legacy = 'legacy',
SecretsManager = 'secretsmanager',
}

14
src/main.ts Normal file
View File

@ -0,0 +1,14 @@
import { ClassSerializerInterceptor } from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import * as morgan from 'morgan';
(async () => {
const port = 8081;
const app = await NestFactory.create(AppModule);
app.use(morgan('dev'));
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.listen(port, () => console.log(`Listening on port ${port}`));
})();

View File

@ -0,0 +1,9 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('secret')
export class Secret extends BaseEntity {
@PrimaryGeneratedColumn({ name: 'id' })
id: string;
}

View File

@ -0,0 +1,5 @@
import { AbstractActionHandler } from '../abstract-action.handler';
import { Action } from '../action.enum';
export type SecretsManagerHandlers = Record<Action, AbstractActionHandler>;
export const SecretsManagerHandlers = Symbol.for('SecretsManagerHandlers');

View File

@ -0,0 +1,51 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
import { SecretsManagerHandlers } from './secrets-manager.constants';
const handlers = [
]
const actions = [
Action.SecretsManagerCancelRotateSecret,
Action.SecretsManagerCreateSecret,
Action.SecretsManagerDeleteResourcePolicy,
Action.SecretsManagerDeleteSecret,
Action.SecretsManagerDescribeSecret,
Action.SecretsManagerGetRandomPassword,
Action.SecretsManagerGetResourcePolicy,
Action.SecretsManagerGetSecretValue,
Action.SecretsManagerListSecrets,
Action.SecretsManagerListSecretVersionIds,
Action.SecretsManagerPutResourcePolicy,
Action.SecretsManagerPutSecretValue,
Action.SecretsManagerRemoveRegionsFromReplication,
Action.SecretsManagerReplicateSecretToRegions,
Action.SecretsManagerRestoreSecret,
Action.SecretsManagerRotateSecret,
Action.SecretsManagerStopReplicationToReplica,
Action.SecretsManagerTagResource,
Action.SecretsManagerUntagResource,
Action.SecretsManagerUpdateSecret,
Action.SecretsManagerUpdateSecretVersionStage,
Action.SecretsManagerValidateResourcePolicy,
]
@Module({
imports: [
TypeOrmModule.forFeature([]),
AwsSharedEntitiesModule,
],
providers: [
...handlers,
ExistingActionHandlersProvider(handlers),
DefaultActionHandlerProvider(SecretsManagerHandlers, Format.Json, actions),
],
exports: [SecretsManagerHandlers],
})
export class SecretsManagerModule {}

View File

@ -0,0 +1,44 @@
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 { SnsTopic } from './sns-topic.entity';
import * as Joi from 'joi';
import { TagsService } from '../aws-shared-entities/tags.service';
type QueryParams = {
Name: string;
}
@Injectable()
export class CreateTopicHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(SnsTopic)
private readonly snsTopicRepo: Repository<SnsTopic>,
private readonly tagsService: TagsService,
) {
super();
}
format = Format.Xml;
action = Action.SnsCreateTopic;
validator = Joi.object<QueryParams, true>({ Name: Joi.string().required() });
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
const { Name: name } = params;
const topic = await this.snsTopicRepo.create({
name,
accountId: awsProperties.accountId,
region: awsProperties.region,
}).save();
const tags = TagsService.tagPairs(params);
await this.tagsService.createMany(topic.topicArn, tags);
return { TopicArn: topic.topicArn };
}
}

View File

@ -0,0 +1,51 @@
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 { SnsTopicSubscription } from './sns-topic-subscription.entity';
import { AttributesService } from '../aws-shared-entities/attributes.service';
type QueryParams = {
SubscriptionArn: string;
}
@Injectable()
export class GetSubscriptionAttributesHandler extends AbstractActionHandler {
constructor(
@InjectRepository(SnsTopicSubscription)
private readonly snsTopicSubscriptionRepo: Repository<SnsTopicSubscription>,
private readonly attributeService: AttributesService,
) {
super();
}
format = Format.Xml;
action = Action.SnsGetSubscriptionAttributes;
validator = Joi.object<QueryParams, true>({ SubscriptionArn: Joi.string().required() });
protected async handle({ SubscriptionArn }: QueryParams, awsProperties: AwsProperties) {
const id = SubscriptionArn.split(':')[-1];
const subscription = await this.snsTopicSubscriptionRepo.findOne({ where: { id }});
const attributes = await this.attributeService.getByArn(SubscriptionArn);
const attributeMap = attributes.reduce((m, a) => {
m[a.name] = a.value;
return m;
}, {});
const response = {
ConfirmationWasAuthenticated: 'true',
PendingConfirmation: 'false',
Owner: subscription.accountId,
SubscriptionArn: subscription.arn,
TopicArn: subscription.topicArn,
...attributeMap,
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
}
return AttributesService.getXmlSafeAttributesMap(response);
}
}

View File

@ -0,0 +1,52 @@
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 { SnsTopic } from './sns-topic.entity';
import * as Joi from 'joi';
import { AttributesService } from '../aws-shared-entities/attributes.service';
type QueryParams = {
TopicArn: string;
}
@Injectable()
export class GetTopicAttributesHandler extends AbstractActionHandler {
constructor(
@InjectRepository(SnsTopic)
private readonly snsTopicRepo: Repository<SnsTopic>,
private readonly attributeService: AttributesService,
) {
super();
}
format = Format.Xml;
action = Action.SnsGetTopicAttributes;
validator = Joi.object<QueryParams, true>({ TopicArn: Joi.string().required() });
protected async handle({ TopicArn }: QueryParams, awsProperties: AwsProperties) {
const name = TopicArn.split(':')[-1];
const topic = await this.snsTopicRepo.findOne({ where: { name }});
const attributes = await this.attributeService.getByArn(TopicArn);
const attributeMap = attributes.reduce((m, a) => {
m[a.name] = a.value;
return m;
}, {});
const response = {
DisplayName: topic.name,
Owner: topic.accountId,
SubscriptionsConfirmed: '0',
SubscriptionsDeleted: '0',
SubscriptionsPending: '0',
TopicArn: topic.topicArn,
...attributeMap,
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
}
return AttributesService.getXmlSafeAttributesMap(response);
}
}

View File

@ -0,0 +1,31 @@
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 { SnsTopic } from './sns-topic.entity';
import * as Joi from 'joi';
import { TagsService } from '../aws-shared-entities/tags.service';
type QueryParams = {
ResourceArn: string;
}
@Injectable()
export class ListTagsForResourceHandler extends AbstractActionHandler {
constructor(
private readonly tagsService: TagsService,
) {
super();
}
format = Format.Xml;
action = Action.SnsListTagsForResource;
validator = Joi.object<QueryParams, true>({ ResourceArn: Joi.string().required() });
protected async handle({ ResourceArn }: QueryParams, awsProperties: AwsProperties) {
const tags = await this.tagsService.getByArn(ResourceArn);
return TagsService.getXmlSafeAttributesMap(tags);
}
}

View File

@ -0,0 +1,41 @@
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 { SnsTopic } from './sns-topic.entity';
import * as Joi from 'joi';
type QueryParams = {
NextToken: number;
}
@Injectable()
export class ListTopicsHandler extends AbstractActionHandler {
constructor(
@InjectRepository(SnsTopic)
private readonly snsTopicRepo: Repository<SnsTopic>,
) {
super();
}
format = Format.Xml;
action = Action.SnsListTopics;
validator = Joi.object<QueryParams, true>({ NextToken: Joi.number().default(0) });
protected async handle({ NextToken: skip }: QueryParams, awsProperties: AwsProperties) {
const [ topics, total ] = await this.snsTopicRepo.findAndCount({ order: { name: 'DESC' }, take: 100, skip });
const response = { Topics: topics.map(t => ({ Topic: { TopicArn: t.topicArn } }))};
if (total >= 100) {
return {
...response,
NextToken: `${skip + 100}`
}
}
return response;
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { AttributesService } from '../aws-shared-entities/attributes.service';
type QueryParams = {
AttributeName: string;
AttributeValue: string;
TopicArn: string;
}
@Injectable()
export class SetSubscriptionAttributesHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly attributeService: AttributesService,
) {
super();
}
format = Format.Xml;
action = Action.SnsSetSubscriptionAttributes;
validator = Joi.object<QueryParams, true>({
AttributeName: Joi.string().required(),
AttributeValue: Joi.string().required(),
TopicArn: Joi.string().required(),
});
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, awsProperties: AwsProperties) {
await this.attributeService.create({ name: AttributeName, value: AttributeValue, arn: TopicArn });
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { AttributesService } from '../aws-shared-entities/attributes.service';
type QueryParams = {
AttributeName: string;
AttributeValue: string;
TopicArn: string;
}
@Injectable()
export class SetTopicAttributesHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly attributeService: AttributesService,
) {
super();
}
format = Format.Xml;
action = Action.SnsSetTopicAttributes;
validator = Joi.object<QueryParams, true>({
AttributeName: Joi.string().required(),
AttributeValue: Joi.string().required(),
TopicArn: Joi.string().required(),
});
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, awsProperties: AwsProperties) {
await this.attributeService.create({ name: AttributeName, value: AttributeValue, arn: TopicArn });
}
}

View File

@ -0,0 +1,27 @@
import { BaseEntity, Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
@Entity('sns_topic_subscription')
export class SnsTopicSubscription extends BaseEntity {
@PrimaryColumn({ name: 'id' })
id: string;
@Column({ name: 'topic_arn' })
topicArn: string;
@Column({ name: 'endpoint', nullable: true })
endpoint: string;
@Column({ name: 'protocol' })
protocol: string;
@Column({ name: 'account_id', nullable: false })
accountId: string;
@Column({ name: 'region', nullable: false })
region: string;
get arn() {
return `${this.topicArn}:${this.id}`;
}
}

View File

@ -0,0 +1,18 @@
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('sns_topic')
export class SnsTopic extends BaseEntity {
@PrimaryColumn({ name: 'name' })
name: string;
@Column({ name: 'account_id', nullable: false })
accountId: string;
@Column({ name: 'region', nullable: false })
region: string;
get topicArn(): string {
return `arn:aws:sns:${this.region}:${this.accountId}:${this.name}`;
}
}

5
src/sns/sns.constants.ts Normal file
View File

@ -0,0 +1,5 @@
import { AbstractActionHandler } from '../abstract-action.handler';
import { Action } from '../action.enum';
export type SnsHandlers = Record<Action, AbstractActionHandler>;
export const SnsHandlers = Symbol.for('SNS_HANDLERS');

91
src/sns/sns.module.ts Normal file
View File

@ -0,0 +1,91 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AbstractActionHandler, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
import { ExistingActionHandlers } from '../default-action-handler/default-action-handler.constants';
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
import { CreateTopicHandler } from './create-topic.handler';
import { GetSubscriptionAttributesHandler } from './get-subscription-attributes.handler';
import { GetTopicAttributesHandler } from './get-topic-attributes.handler';
import { ListTagsForResourceHandler } from './list-tags-for-resource.handler';
import { ListTopicsHandler } from './list-topics.handler';
import { SetSubscriptionAttributesHandler } from './set-subscription-attributes.handler';
import { SetTopicAttributesHandler } from './set-topic-attributes.handler';
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
import { SnsTopic } from './sns-topic.entity';
import { SnsHandlers } from './sns.constants';
import { SubscribeHandler } from './subscribe.handler';
const handlers = [
CreateTopicHandler,
GetSubscriptionAttributesHandler,
GetTopicAttributesHandler,
ListTagsForResourceHandler,
ListTopicsHandler,
SetSubscriptionAttributesHandler,
SetTopicAttributesHandler,
SubscribeHandler,
];
const actions = [
Action.SnsAddPermission,
Action.SnsCheckIfPhoneNumberIsOptedOut,
Action.SnsConfirmSubscription,
Action.SnsCreatePlatformApplication,
Action.SnsCreatePlatformEndpoint,
Action.SnsCreateSMSSandboxPhoneNumber,
Action.SnsCreateTopic,
Action.SnsDeleteEndpoint,
Action.SnsDeletePlatformApplication,
Action.SnsDeleteSMSSandboxPhoneNumber,
Action.SnsDeleteTopic,
Action.SnsGetDataProtectionPolicy,
Action.SnsGetEndpointAttributes,
Action.SnsGetPlatformApplicationAttributes,
Action.SnsGetSMSAttributes,
Action.SnsGetSMSSandboxAccountStatus,
Action.SnsGetSubscriptionAttributes,
Action.SnsGetTopicAttributes,
Action.SnsListEndpointsByPlatformApplication,
Action.SnsListOriginationNumbers,
Action.SnsListPhoneNumbersOptedOut,
Action.SnsListPlatformApplications,
Action.SnsListSMSSandboxPhoneNumbers,
Action.SnsListSubscriptions,
Action.SnsListSubscriptionsByTopic,
Action.SnsListTagsForResource,
Action.SnsListTopics,
Action.SnsOptInPhoneNumber,
Action.SnsPublish,
Action.SnsPublishBatch,
Action.SnsPutDataProtectionPolicy,
Action.SnsRemovePermission,
Action.SnsSetEndpointAttributes,
Action.SnsSetPlatformApplicationAttributes,
Action.SnsSetSMSAttributes,
Action.SnsSetSubscriptionAttributes,
Action.SnsSetTopicAttributes,
Action.SnsSubscribe,
Action.SnsTagResource,
Action.SnsUnsubscribe,
Action.SnsUntagResource,
Action.SnsVerifySMSSandboxPhoneNumber,
]
@Module({
imports: [
TypeOrmModule.forFeature([SnsTopic, SnsTopicSubscription]),
AwsSharedEntitiesModule,
],
providers: [
...handlers,
ExistingActionHandlersProvider(handlers),
DefaultActionHandlerProvider(SnsHandlers, Format.Xml, actions),
],
exports: [
SnsHandlers,
]
})
export class SnsModule {}

View File

@ -0,0 +1,57 @@
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 { TagsService } from '../aws-shared-entities/tags.service';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
import { uuid } from 'uuidv4';
type QueryParams = {
TopicArn: string;
Protocol: string;
Endpoint?: string;
}
@Injectable()
export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(SnsTopicSubscription)
private readonly snsTopicSubscription: Repository<SnsTopicSubscription>,
private readonly tagsService: TagsService,
private readonly attributeService: AttributesService,
) {
super();
}
format = Format.Xml;
action = Action.SnsSubscribe;
validator = Joi.object<QueryParams, true>({
Endpoint: Joi.string(),
TopicArn: Joi.string().required(),
Protocol: Joi.string().required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
const subscription = await this.snsTopicSubscription.create({
id: uuid(),
topicArn: params.TopicArn,
protocol: params.Protocol,
endpoint: params.Endpoint,
accountId: awsProperties.accountId,
region: awsProperties.region,
}).save();
const tags = TagsService.tagPairs(params);
await this.tagsService.createMany(subscription.arn, tags);
const attributes = AttributesService.attributePairs(params);
await this.attributeService.createMany(subscription.arn, attributes);
return { SubscriptionArn: subscription.arn };
}
}

0
src/sqs/sqs.module.ts Normal file
View File

4
tsconfig.build.ts Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "dist"]
}

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"resolveJsonModule": true,
},
"exclude": ["node_modules"],
"include": ["src/**/*.ts"]
}

3283
yarn.lock Normal file

File diff suppressed because it is too large Load Diff