added readme and improved config

This commit is contained in:
Matthew Bessette 2023-03-23 11:59:08 -04:00
parent 8389db4367
commit a6524d7f65
14 changed files with 229 additions and 27 deletions

2
.gitignore vendored
View File

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

37
README.md Normal file
View File

@ -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<string, any> | void
* the method that implements the AWS action

View File

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

View File

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

View File

@ -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<CommonConfig>) => ({
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,
],
},
],

View File

@ -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<T> implements NestInterceptor<T, Response> {
@ -11,10 +13,15 @@ export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
constructor(
@InjectRepository(Audit)
private readonly auditRepo: Repository<Audit>,
private readonly configService: ConfigService<CommonConfig>,
) {}
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
if (!this.configService.get('AUDIT')) {
return next.handle();
}
const requestId = uuid.v4();
const httpContext = context.switchToHttp();

View File

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

View File

@ -0,0 +1,14 @@
import * as Joi from 'joi';
import { CommonConfig } from './common-config.interface';
export const configValidator = Joi.object<CommonConfig, true>({
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'),
});

View File

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

View File

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

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

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

79
src/kms/kms.module.ts Normal file
View File

@ -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 {}

View File

@ -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<CommonConfig> = app.get(ConfigService)
await app.listen(configService.get('PORT'), () => console.log(`Listening on port ${configService.get('PORT')}`));
})();

View File

@ -27,19 +27,24 @@ export class SetQueueAttributesHandler extends AbstractActionHandler<QueryParams
format = Format.Xml;
action = Action.SqsSetQueueAttributes;
validator = Joi.object<QueryParams, true>({
'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);
}
}