added readme and improved config
This commit is contained in:
parent
8389db4367
commit
a6524d7f65
|
|
@ -1,3 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
./local-aws.sqlite
|
||||
data
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
});
|
||||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
export enum Group {
|
||||
Legacy = 'legacy',
|
||||
SecretsManager = 'secretsmanager',
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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')}`));
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue