From a6524d7f657745d6f320b8189603e5426b5a96d7 Mon Sep 17 00:00:00 2001 From: Matthew Bessette Date: Thu, 23 Mar 2023 11:59:08 -0400 Subject: [PATCH] added readme and improved config --- .gitignore | 2 +- README.md | 37 ++++++++++++ src/action.enum.ts | 52 ++++++++++++++++ src/app.controller.ts | 12 +--- src/app.module.ts | 9 ++- src/audit/audit.interceptor.ts | 7 +++ src/config/common-config.interface.ts | 3 + src/config/config.validator.ts | 14 +++++ src/config/local.config.ts | 14 +++-- src/group.enum.ts | 4 -- src/kms/kms.constants.ts | 5 ++ src/kms/kms.module.ts | 79 +++++++++++++++++++++++++ src/main.ts | 7 ++- src/sqs/set-queue-attributes.handler.ts | 11 +++- 14 files changed, 229 insertions(+), 27 deletions(-) create mode 100644 README.md create mode 100644 src/config/config.validator.ts delete mode 100644 src/group.enum.ts create mode 100644 src/kms/kms.constants.ts create mode 100644 src/kms/kms.module.ts diff --git a/.gitignore b/.gitignore index a45c268..20172f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules dist -./local-aws.sqlite +data diff --git a/README.md b/README.md new file mode 100644 index 0000000..1dbeee5 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Local AWS + +## Running +### Environment Variables +| Variable | Description | Default | +| -------------- | --------------------------------------------------- | -------------- | +| AWS_ACCOUNT_ID | AWS Account ID resources will default to | 000000000000 | +| AWS_REGION | AWS Region resources will default to | us-east-1 | +| DB_DATABASE | SQLITE database to write to, defaults to in memory | :memory: | +| DEBUG | Whether or not to reveal sql and other audit trails | false | +| HOST | Used in URL generation | localhost | +| PORT | Used in URL generation & runtime port binding | 8081 | +| PROTO | Used in URL generation | http | + +## Contributing +### Design Pattern +Handler / Visitor Pattern + +Relying on Nest.js's dependency tree and injection, handlers are loaded as singletons and pulled in to a map key'd by AWS's own Action names. + +Actions are defined in src/action.enum.ts + +app.controller.ts is the entry point for all AWS API calls + +Each action is implemented via it's respective handler. Use `aws sns create-topic` as an example: +app.module.ts loads sns.module.ts +app.module.ts injects SnsHandlers +SnsHandlers is a a map of implemented and mocked handlers based on its list of `actions` provided by the sns.module.ts module +sns.module.ts has a list of handlers that have been implemented, including create-topic.handler.ts +create-topic.handler.ts extends abstract-action.handler.ts + +abstract-action.handler.ts +* format: the format for output (XML or JSON) +* action: the action the handler is implementing (will be use to key by) +* validator: the Joi validator to be executed to check for required params +* handle(queryParams: T, awsProperties: AwsProperties): Record | void + * the method that implements the AWS action diff --git a/src/action.enum.ts b/src/action.enum.ts index 122a5b4..7e0fed5 100644 --- a/src/action.enum.ts +++ b/src/action.enum.ts @@ -1,5 +1,57 @@ export enum Action { + // KMS + KmsCancelKeyDeletion = 'CancelKeyDeletion', + KmsConnectCustomKeyStore = 'ConnectCustomKeyStore', + KmsCreateAlias = 'CreateAlias', + KmsCreateCustomKeyStore = 'CreateCustomKeyStore', + KmsCreateGrant = 'CreateGrant', + KmsCreateKey = 'CreateKey', + KmsDecrypt = 'Decrypt', + KmsDeleteAlias = 'DeleteAlias', + KmsDeleteCustomKeyStore = 'DeleteCustomKeyStore', + KmsDeleteImportedKeyMaterial = 'DeleteImportedKeyMaterial', + KmsDescribeCustomKeyStores = 'DescribeCustomKeyStores', + KmsDescribeKey = 'DescribeKey', + KmsDisableKey = 'DisableKey', + KmsDisableKeyRotation = 'DisableKeyRotation', + KmsDisconnectCustomKeyStore = 'DisconnectCustomKeyStore', + KmsEnableKey = 'EnableKey', + KmsEnableKeyRotation = 'EnableKeyRotation', + KmsEncrypt = 'Encrypt', + KmsGenerateDataKey = 'GenerateDataKey', + KmsGenerateDataKeyPair = 'GenerateDataKeyPair', + KmsGenerateDataKeyPairWithoutPlaintext = 'GenerateDataKeyPairWithoutPlaintext', + KmsGenerateDataKeyWithoutPlaintext = 'GenerateDataKeyWithoutPlaintext', + KmsGenerateMac = 'GenerateMac', + KmsGenerateRandom = 'GenerateRandom', + KmsGetKeyPolicy = 'GetKeyPolicy', + KmsGetKeyRotationStatus = 'GetKeyRotationStatus', + KmsGetParametersForImport = 'GetParametersForImport', + KmsGetPublicKey = 'GetPublicKey', + KmsImportKeyMaterial = 'ImportKeyMaterial', + KmsListAliases = 'ListAliases', + KmsListGrants = 'ListGrants', + KmsListKeyPolicies = 'ListKeyPolicies', + KmsListKeys = 'ListKeys', + KmsListResourceTags = 'ListResourceTags', + KmsListRetirableGrants = 'ListRetirableGrants', + KmsPutKeyPolicy = 'PutKeyPolicy', + KmsReEncrypt = 'ReEncrypt', + KmsReplicateKey = 'ReplicateKey', + KmsRetireGrant = 'RetireGrant', + KmsRevokeGrant = 'RevokeGrant', + KmsScheduleKeyDeletion = 'ScheduleKeyDeletion', + KmsSign = 'Sign', + KmsTagResource = 'TagResource', + KmsUntagResource = 'UntagResource', + KmsUpdateAlias = 'UpdateAlias', + KmsUpdateCustomKeyStore = 'UpdateCustomKeyStore', + KmsUpdateKeyDescription = 'UpdateKeyDescription', + KmsUpdatePrimaryRegion = 'UpdatePrimaryRegion', + KmsVerify = 'Verify', + KmsVerifyMac = 'VerifyMac', + // SecretsManager SecretsManagerCancelRotateSecret = 'secretsmanager.CancelRotateSecret', SecretsManagerCreateSecret = 'secretsmanager.CreateSecret', diff --git a/src/app.controller.ts b/src/app.controller.ts index a06803b..27486c9 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Body, Controller, Inject, Post, Headers, Header, Req, HttpStatus, HttpCode, UseInterceptors } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Inject, Post, Headers, Req, HttpCode, UseInterceptors } from '@nestjs/common'; import { ActionHandlers } from './app.constants'; import * as Joi from 'joi'; import { Action } from './action.enum'; @@ -6,7 +6,6 @@ import { AbstractActionHandler, Format } from './abstract-action.handler'; import * as js2xmlparser from 'js2xmlparser'; import { ConfigService } from '@nestjs/config'; import { CommonConfig } from './config/common-config.interface'; -import * as uuid from 'uuid'; import { Request } from 'express'; import { AuditInterceptor } from './audit/audit.interceptor'; @@ -34,9 +33,7 @@ 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(), }).validate(queryParams, { allowUnknown: true }); @@ -47,7 +44,6 @@ export class AppController { 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) { @@ -57,14 +53,12 @@ export class AppController { const awsProperties = { accountId: this.configService.get('AWS_ACCOUNT_ID'), region: this.configService.get('AWS_REGION'), - host: this.configService.get('HOST'), + host: `${this.configService.get('PROTO')}://${this.configService.get('HOST')}:${this.configService.get('PORT')}`, }; 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 js2xmlparser.parse(`${handler.action}Response`, jsonResponse); } return jsonResponse; } diff --git a/src/app.module.ts b/src/app.module.ts index 199eaf2..4a72120 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,25 +14,29 @@ import { SqsModule } from './sqs/sqs.module'; import { SqsHandlers } from './sqs/sqs.constants'; import { Audit } from './audit/audit.entity'; import { AuditInterceptor } from './audit/audit.interceptor'; +import { KmsModule } from './kms/kms.module'; +import { KMSHandlers } from './kms/kms.constants'; +import { configValidator } from './config/config.validator'; @Module({ imports: [ ConfigModule.forRoot({ load: [localConfig], isGlobal: true, - // validationSchema: configValidator, + validationSchema: configValidator, }), TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (configService: ConfigService) => ({ type: 'sqlite', - database: configService.get('DB_DATABASE'), + database: configService.get('DB_DATABASE') === ':memory:' ? configService.get('DB_DATABASE') : `${__dirname}/../data/${configService.get('DB_DATABASE')}`, logging: configService.get('DB_LOGGING'), synchronize: configService.get('DB_SYNCHRONIZE'), entities: [__dirname + '/**/*.entity{.ts,.js}'], }), }), TypeOrmModule.forFeature([Audit]), + KmsModule, SecretsManagerModule, SnsModule, SqsModule, @@ -50,6 +54,7 @@ import { AuditInterceptor } from './audit/audit.interceptor'; SnsHandlers, SqsHandlers, SecretsManagerHandlers, + KMSHandlers, ], }, ], diff --git a/src/audit/audit.interceptor.ts b/src/audit/audit.interceptor.ts index 5ba50b2..9bf3e17 100644 --- a/src/audit/audit.interceptor.ts +++ b/src/audit/audit.interceptor.ts @@ -4,6 +4,8 @@ import { Observable, tap } from 'rxjs'; import { Repository } from 'typeorm'; import { Audit } from './audit.entity'; import * as uuid from 'uuid'; +import { ConfigService } from '@nestjs/config'; +import { CommonConfig } from '../config/common-config.interface'; @Injectable() export class AuditInterceptor implements NestInterceptor { @@ -11,10 +13,15 @@ export class AuditInterceptor implements NestInterceptor { constructor( @InjectRepository(Audit) private readonly auditRepo: Repository, + private readonly configService: ConfigService, ) {} intercept(context: ExecutionContext, next: CallHandler): Observable { + if (!this.configService.get('AUDIT')) { + return next.handle(); + } + const requestId = uuid.v4(); const httpContext = context.switchToHttp(); diff --git a/src/config/common-config.interface.ts b/src/config/common-config.interface.ts index d55ee3e..8d4ed34 100644 --- a/src/config/common-config.interface.ts +++ b/src/config/common-config.interface.ts @@ -1,8 +1,11 @@ export interface CommonConfig { + AUDIT: boolean; AWS_ACCOUNT_ID: string; AWS_REGION: string; DB_DATABASE: string; DB_LOGGING?: boolean; DB_SYNCHRONIZE?: boolean; HOST: string; + PORT: number; + PROTO: string; } diff --git a/src/config/config.validator.ts b/src/config/config.validator.ts new file mode 100644 index 0000000..925008c --- /dev/null +++ b/src/config/config.validator.ts @@ -0,0 +1,14 @@ +import * as Joi from 'joi'; +import { CommonConfig } from './common-config.interface'; + +export const configValidator = Joi.object({ + AUDIT: Joi.boolean().default(false), + AWS_ACCOUNT_ID: Joi.string().default('000000000000'), + AWS_REGION: Joi.string().default('us-east-1'), + DB_DATABASE: Joi.string().default(':memory:'), + DB_LOGGING: Joi.boolean().default(false), + DB_SYNCHRONIZE: Joi.boolean().default(true), + HOST: Joi.string().default('localhost'), + PORT: Joi.number().default(8081), + PROTO: Joi.string().valid('http', 'https').default('http'), +}); diff --git a/src/config/local.config.ts b/src/config/local.config.ts index 9d6712c..3053444 100644 --- a/src/config/local.config.ts +++ b/src/config/local.config.ts @@ -1,11 +1,13 @@ import { CommonConfig } from "./common-config.interface"; export default (): CommonConfig => ({ - AWS_ACCOUNT_ID: '000000000000', - AWS_REGION: 'us-east-1', - // DB_DATABASE: ':memory:', - DB_DATABASE: 'local-aws.sqlite', - DB_LOGGING: true, + AUDIT: process.env.DEBUG ? true : false, + AWS_ACCOUNT_ID: process.env.AWS_ACCOUNT_ID, + AWS_REGION: process.env.AWS_REGION, + DB_DATABASE: process.env.PERSISTANCE, + DB_LOGGING: process.env.DEBUG ? true : false, DB_SYNCHRONIZE: true, - HOST: 'http://localhost:8081', + HOST: process.env.HOST, + PROTO: process.env.PROTOCOL, + PORT: Number(process.env.PORT), }); diff --git a/src/group.enum.ts b/src/group.enum.ts deleted file mode 100644 index 3ebbe60..0000000 --- a/src/group.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum Group { - Legacy = 'legacy', - SecretsManager = 'secretsmanager', -} diff --git a/src/kms/kms.constants.ts b/src/kms/kms.constants.ts new file mode 100644 index 0000000..de33432 --- /dev/null +++ b/src/kms/kms.constants.ts @@ -0,0 +1,5 @@ +import { AbstractActionHandler } from '../abstract-action.handler'; +import { Action } from '../action.enum'; + +export type KMSHandlers = Record; +export const KMSHandlers = Symbol.for('KMSHandlers'); diff --git a/src/kms/kms.module.ts b/src/kms/kms.module.ts new file mode 100644 index 0000000..d8af731 --- /dev/null +++ b/src/kms/kms.module.ts @@ -0,0 +1,79 @@ +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 { KMSHandlers } from './kms.constants'; + +const handlers = [ + +] + +const actions = [ + Action.KmsCancelKeyDeletion, + Action.KmsConnectCustomKeyStore, + Action.KmsCreateAlias, + Action.KmsCreateCustomKeyStore, + Action.KmsCreateGrant, + Action.KmsCreateKey, + Action.KmsDecrypt, + Action.KmsDeleteAlias, + Action.KmsDeleteCustomKeyStore, + Action.KmsDeleteImportedKeyMaterial, + Action.KmsDescribeCustomKeyStores, + Action.KmsDescribeKey, + Action.KmsDisableKey, + Action.KmsDisableKeyRotation, + Action.KmsDisconnectCustomKeyStore, + Action.KmsEnableKey, + Action.KmsEnableKeyRotation, + Action.KmsEncrypt, + Action.KmsGenerateDataKey, + Action.KmsGenerateDataKeyPair, + Action.KmsGenerateDataKeyPairWithoutPlaintext, + Action.KmsGenerateDataKeyWithoutPlaintext, + Action.KmsGenerateMac, + Action.KmsGenerateRandom, + Action.KmsGetKeyPolicy, + Action.KmsGetKeyRotationStatus, + Action.KmsGetParametersForImport, + Action.KmsGetPublicKey, + Action.KmsImportKeyMaterial, + Action.KmsListAliases, + Action.KmsListGrants, + Action.KmsListKeyPolicies, + Action.KmsListKeys, + Action.KmsListResourceTags, + Action.KmsListRetirableGrants, + Action.KmsPutKeyPolicy, + Action.KmsReEncrypt, + Action.KmsReplicateKey, + Action.KmsRetireGrant, + Action.KmsRevokeGrant, + Action.KmsScheduleKeyDeletion, + Action.KmsSign, + Action.KmsTagResource, + Action.KmsUntagResource, + Action.KmsUpdateAlias, + Action.KmsUpdateCustomKeyStore, + Action.KmsUpdateKeyDescription, + Action.KmsUpdatePrimaryRegion, + Action.KmsVerify, + Action.KmsVerifyMac, +] + +@Module({ + imports: [ + TypeOrmModule.forFeature([]), + AwsSharedEntitiesModule, + ], + providers: [ + ...handlers, + ExistingActionHandlersProvider(handlers), + DefaultActionHandlerProvider(KMSHandlers, Format.Json, actions), + ], + exports: [KMSHandlers], +}) +export class KmsModule {} diff --git a/src/main.ts b/src/main.ts index 41abb7a..538adc5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,15 +2,18 @@ import { ClassSerializerInterceptor } from '@nestjs/common'; import { NestFactory, Reflector } from '@nestjs/core'; import { AppModule } from './app.module'; import * as morgan from 'morgan'; +import { CommonConfig } from './config/common-config.interface'; +import { ConfigService } from '@nestjs/config'; const bodyParser = require('body-parser'); (async () => { - const port = 8081; const app = await NestFactory.create(AppModule); app.use(morgan('dev')); app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); app.use(bodyParser.json({ type: 'application/x-amz-json-1.1'})); - await app.listen(port, () => console.log(`Listening on port ${port}`)); + const configService: ConfigService = app.get(ConfigService) + + await app.listen(configService.get('PORT'), () => console.log(`Listening on port ${configService.get('PORT')}`)); })(); diff --git a/src/sqs/set-queue-attributes.handler.ts b/src/sqs/set-queue-attributes.handler.ts index 990fe29..6aa5e38 100644 --- a/src/sqs/set-queue-attributes.handler.ts +++ b/src/sqs/set-queue-attributes.handler.ts @@ -27,19 +27,24 @@ export class SetQueueAttributesHandler extends AbstractActionHandler({ - 'Attribute.Name': Joi.string().required(), - 'Attribute.Value': Joi.string().required(), + 'Attribute.Name': Joi.string(), + 'Attribute.Value': Joi.string(), __path: Joi.string().required(), }); protected async handle(params: QueryParams, awsProperties: AwsProperties) { const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(params.__path); const queue = await this.sqsQueueRepo.findOne({ where: { accountId , name } }); + const attributes = SqsQueue.attributePairs(params); + + if (params['Attribute.Name'] && params['Attribute.Value']) { + attributes.push({ key: params['Attribute.Name'], value: params['Attribute.Value'] }); + } if(!queue) { throw new BadRequestException('ResourceNotFoundException'); } - await this.attributeService.create({ name: params['Attribute.Name'], value: params['Attribute.Value'], arn: queue.arn }); + await this.attributeService.createMany(queue.arn, attributes); } }