Audit logging, secrets manager completion, sns and sqs wrapping up
This commit is contained in:
parent
07d3841cd7
commit
ee0babc2e3
|
|
@ -13,6 +13,7 @@
|
||||||
"@nestjs/core": "^9.3.10",
|
"@nestjs/core": "^9.3.10",
|
||||||
"@nestjs/platform-express": "^9.3.10",
|
"@nestjs/platform-express": "^9.3.10",
|
||||||
"@nestjs/typeorm": "^9.0.1",
|
"@nestjs/typeorm": "^9.0.1",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"joi": "^17.9.0",
|
"joi": "^17.9.0",
|
||||||
"js2xmlparser": "^5.0.0",
|
"js2xmlparser": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import * as Joi from 'joi';
|
||||||
export type AwsProperties = {
|
export type AwsProperties = {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
region: string;
|
region: string;
|
||||||
|
host: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Format {
|
export enum Format {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { BadRequestException, Body, Controller, Get, Inject, Post, Headers, Header } from '@nestjs/common';
|
import { BadRequestException, Body, Controller, Inject, Post, Headers, Header, Req, HttpStatus, HttpCode, UseInterceptors } from '@nestjs/common';
|
||||||
import { ActionHandlers } from './app.constants';
|
import { ActionHandlers } from './app.constants';
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import { Action } from './action.enum';
|
import { Action } from './action.enum';
|
||||||
|
|
@ -7,6 +7,8 @@ import * as js2xmlparser from 'js2xmlparser';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { CommonConfig } from './config/common-config.interface';
|
import { CommonConfig } from './config/common-config.interface';
|
||||||
import * as uuid from 'uuid';
|
import * as uuid from 'uuid';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { AuditInterceptor } from './audit/audit.interceptor';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
|
|
@ -18,8 +20,10 @@ export class AppController {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@Header('x-amzn-RequestId', uuid.v4())
|
@HttpCode(200)
|
||||||
|
@UseInterceptors(AuditInterceptor)
|
||||||
async post(
|
async post(
|
||||||
|
@Req() request: Request,
|
||||||
@Body() body: Record<string, any>,
|
@Body() body: Record<string, any>,
|
||||||
@Headers() headers: Record<string, any>,
|
@Headers() headers: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -29,7 +33,7 @@ export class AppController {
|
||||||
return o;
|
return o;
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
const queryParams = { ...body, ...lowerCasedHeaders };
|
const queryParams = { __path: request.path, ...body, ...lowerCasedHeaders };
|
||||||
console.log({queryParams})
|
console.log({queryParams})
|
||||||
const actionKey = queryParams['x-amz-target'] ? 'x-amz-target' : 'Action';
|
const actionKey = queryParams['x-amz-target'] ? 'x-amz-target' : 'Action';
|
||||||
|
|
||||||
|
|
@ -50,7 +54,11 @@ export class AppController {
|
||||||
throw new BadRequestException(validatorError);
|
throw new BadRequestException(validatorError);
|
||||||
}
|
}
|
||||||
|
|
||||||
const awsProperties = { accountId: this.configService.get('AWS_ACCOUNT_ID'), region: this.configService.get('AWS_REGION') };
|
const awsProperties = {
|
||||||
|
accountId: this.configService.get('AWS_ACCOUNT_ID'),
|
||||||
|
region: this.configService.get('AWS_REGION'),
|
||||||
|
host: this.configService.get('HOST'),
|
||||||
|
};
|
||||||
|
|
||||||
const jsonResponse = await handler.getResponse(validQueryParams, awsProperties);
|
const jsonResponse = await handler.getResponse(validQueryParams, awsProperties);
|
||||||
if (handler.format === Format.Xml) {
|
if (handler.format === Format.Xml) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ import { AppController } from './app.controller';
|
||||||
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
|
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
|
||||||
import { SecretsManagerModule } from './secrets-manager/secrets-manager.module';
|
import { SecretsManagerModule } from './secrets-manager/secrets-manager.module';
|
||||||
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
|
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
|
||||||
|
import { SqsModule } from './sqs/sqs.module';
|
||||||
|
import { SqsHandlers } from './sqs/sqs.constants';
|
||||||
|
import { Audit } from './audit/audit.entity';
|
||||||
|
import { AuditInterceptor } from './audit/audit.interceptor';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -28,19 +32,23 @@ import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.consta
|
||||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
SnsModule,
|
TypeOrmModule.forFeature([Audit]),
|
||||||
SecretsManagerModule,
|
SecretsManagerModule,
|
||||||
|
SnsModule,
|
||||||
|
SqsModule,
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
AppController,
|
AppController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
AuditInterceptor,
|
||||||
{
|
{
|
||||||
provide: ActionHandlers,
|
provide: ActionHandlers,
|
||||||
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
|
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
|
||||||
inject: [
|
inject: [
|
||||||
SnsHandlers,
|
SnsHandlers,
|
||||||
|
SqsHandlers,
|
||||||
SecretsManagerHandlers,
|
SecretsManagerHandlers,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('audit')
|
||||||
|
export class Audit extends BaseEntity {
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
action: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
request: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
response: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Observable, tap } from 'rxjs';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Audit } from './audit.entity';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Audit)
|
||||||
|
private readonly auditRepo: Repository<Audit>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
|
||||||
|
|
||||||
|
const requestId = uuid.v4();
|
||||||
|
const httpContext = context.switchToHttp();
|
||||||
|
|
||||||
|
const request = httpContext.getRequest();
|
||||||
|
const targetHeaderKey = Object.keys(request.headers).find( k => k.toLocaleLowerCase() === 'x-amz-target');
|
||||||
|
const action = request.headers[targetHeaderKey] ? request.headers[targetHeaderKey] : request.body.Action;
|
||||||
|
|
||||||
|
const response = context.switchToHttp().getResponse();
|
||||||
|
|
||||||
|
response.header('x-amzn-RequestId', requestId);
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap(async (data) => {
|
||||||
|
await this.auditRepo.create({
|
||||||
|
id: requestId,
|
||||||
|
action,
|
||||||
|
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
|
||||||
|
response: JSON.stringify(data),
|
||||||
|
}).save();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ import { Repository } from 'typeorm';
|
||||||
import { Attribute } from './attributes.entity';
|
import { Attribute } from './attributes.entity';
|
||||||
import { CreateAttributeDto } from './create-attribute.dto';
|
import { CreateAttributeDto } from './create-attribute.dto';
|
||||||
|
|
||||||
|
const ResourcePolicyName = 'ResourcePolicy';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttributesService {
|
export class AttributesService {
|
||||||
|
|
||||||
|
|
@ -16,6 +18,14 @@ export class AttributesService {
|
||||||
return await this.repo.find({ where: { arn }});
|
return await this.repo.find({ where: { arn }});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getResourcePolicyByArn(arn: string): Promise<Attribute> {
|
||||||
|
return await this.repo.findOne({ where: { arn, name: ResourcePolicyName }});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createResourcePolicy(arn: string, value: string): Promise<Attribute> {
|
||||||
|
return await this.create({arn, value, name: ResourcePolicyName });
|
||||||
|
}
|
||||||
|
|
||||||
async create(dto: CreateAttributeDto): Promise<Attribute> {
|
async create(dto: CreateAttributeDto): Promise<Attribute> {
|
||||||
return await this.repo.save(dto);
|
return await this.repo.save(dto);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@ export interface CommonConfig {
|
||||||
DB_DATABASE: string;
|
DB_DATABASE: string;
|
||||||
DB_LOGGING?: boolean;
|
DB_LOGGING?: boolean;
|
||||||
DB_SYNCHRONIZE?: boolean;
|
DB_SYNCHRONIZE?: boolean;
|
||||||
|
HOST: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import { CommonConfig } from "./common-config.interface";
|
||||||
export default (): CommonConfig => ({
|
export default (): CommonConfig => ({
|
||||||
AWS_ACCOUNT_ID: '123456789012',
|
AWS_ACCOUNT_ID: '123456789012',
|
||||||
AWS_REGION: 'us-east-1',
|
AWS_REGION: 'us-east-1',
|
||||||
DB_DATABASE: ':memory:', // 'local-aws.sqlite', // :memory:
|
// DB_DATABASE: ':memory:',
|
||||||
|
DB_DATABASE: 'local-aws.sqlite',
|
||||||
DB_LOGGING: true,
|
DB_LOGGING: true,
|
||||||
DB_SYNCHRONIZE: true,
|
DB_SYNCHRONIZE: true,
|
||||||
|
HOST: 'http://localhost:8081',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface CreateSecretDto {
|
||||||
|
versionId?: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
secretString?: string;
|
||||||
|
accountId: string;
|
||||||
|
region: string;
|
||||||
|
}
|
||||||
|
|
@ -5,21 +5,20 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
|
||||||
import { Action } from '../action.enum';
|
import { Action } from '../action.enum';
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import { Secret } from './secret.entity';
|
import { Secret } from './secret.entity';
|
||||||
import * as uuid from 'uuid';
|
import { SecretService } from './secret.service';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
Name: string;
|
Name: string;
|
||||||
Description: string;
|
Description: string;
|
||||||
SecretString: string;
|
SecretString: string;
|
||||||
ClientRequestToken: string;
|
ClientRequestToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
|
export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Secret)
|
private readonly secretService: SecretService,
|
||||||
private readonly secretRepo: Repository<Secret>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -30,21 +29,21 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
|
||||||
Name: Joi.string().required(),
|
Name: Joi.string().required(),
|
||||||
Description: Joi.string().allow('', null),
|
Description: Joi.string().allow('', null),
|
||||||
SecretString: Joi.string().allow('', null),
|
SecretString: Joi.string().allow('', null),
|
||||||
ClientRequestToken: Joi.string().required(),
|
ClientRequestToken: Joi.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params;
|
const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params;
|
||||||
|
|
||||||
const secret = await this.secretRepo.create({
|
const secret = await this.secretService.create({
|
||||||
versionId: ClientRequestToken,
|
versionId: ClientRequestToken,
|
||||||
description,
|
description,
|
||||||
name,
|
name,
|
||||||
secretString,
|
secretString,
|
||||||
accountId: awsProperties.accountId,
|
accountId: awsProperties.accountId,
|
||||||
region: awsProperties.region,
|
region: awsProperties.region,
|
||||||
}).save();
|
});
|
||||||
|
|
||||||
return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name };
|
return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,9 @@ export class DescribeSecretHandler extends AbstractActionHandler {
|
||||||
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
|
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
|
||||||
|
|
||||||
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
|
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
const parts = SecretId.split(':');
|
|
||||||
const name = parts.length > 1 ? parts[-1] : SecretId;
|
|
||||||
|
|
||||||
const secret = await this.secretRepo.findOne({ where: { name } });
|
const name = Secret.getNameFromSecretId(SecretId);
|
||||||
|
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||||
|
|
@ -42,19 +40,18 @@ export class DescribeSecretHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ARN": secret.arn,
|
"ARN": secret.arn,
|
||||||
"CreatedDate": new Date(secret.createdAt).getMilliseconds(),
|
"CreatedDate": new Date(secret.createdAt).toISOString(),
|
||||||
"DeletedDate": 0,
|
"DeletedDate": null,
|
||||||
"Description": secret.description,
|
"Description": secret.description,
|
||||||
"KmsKeyId": "",
|
"KmsKeyId": "",
|
||||||
"LastAccessedDate": new Date().getMilliseconds(),
|
"LastChangedDate": new Date(secret.createdAt).toISOString(),
|
||||||
"LastChangedDate": new Date(secret.createdAt).getMilliseconds(),
|
"LastRotatedDate": null,
|
||||||
"LastRotatedDate": 0,
|
|
||||||
"Name": secret.name,
|
"Name": secret.name,
|
||||||
"OwningService": secret.accountId,
|
"OwningService": secret.accountId,
|
||||||
"PrimaryRegion": secret.region,
|
"PrimaryRegion": secret.region,
|
||||||
"ReplicationStatus": [],
|
"ReplicationStatus": [],
|
||||||
"RotationEnabled": false,
|
"RotationEnabled": false,
|
||||||
"Tags": listOfTagPairs,
|
"Tags": listOfTagPairs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { BadRequestException, 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 { Secret } from './secret.entity';
|
||||||
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
SecretId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetResourcePolicyHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Secret)
|
||||||
|
private readonly secretRepo: Repository<Secret>,
|
||||||
|
private readonly attributesService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.SecretsManagerGetResourcePolicy;
|
||||||
|
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
|
||||||
|
|
||||||
|
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const name = Secret.getNameFromSecretId(SecretId);
|
||||||
|
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute = await this.attributesService.getResourcePolicyByArn(secret.arn);
|
||||||
|
return {
|
||||||
|
ARN: secret.arn,
|
||||||
|
Name: secret.name,
|
||||||
|
ResourcePolicy: attribute?.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 GetSecretValueHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly secretService: SecretService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.SecretsManagerGetSecretValue;
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ARN: secret.arn,
|
||||||
|
CreatedDate: secret.createdAt,
|
||||||
|
Name: secret.name,
|
||||||
|
SecretString: secret.secretString,
|
||||||
|
VersionId: secret.versionId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { BadRequestException, 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 { Secret } from './secret.entity';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
SecretId: string;
|
||||||
|
ResourcePolicy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PutResourcePolicyHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Secret)
|
||||||
|
private readonly secretRepo: Repository<Secret>,
|
||||||
|
private readonly attributesService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.SecretsManagerPutResourcePolicy;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
SecretId: Joi.string().required(),
|
||||||
|
ResourcePolicy: Joi.string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ SecretId, ResourcePolicy }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const name = Secret.getNameFromSecretId(SecretId);
|
||||||
|
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.attributesService.createResourcePolicy(secret.arn, ResourcePolicy);
|
||||||
|
return {
|
||||||
|
ARN: secret.arn,
|
||||||
|
Name: secret.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
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 = {
|
||||||
|
ClientRequestToken?: string;
|
||||||
|
SecretId: string;
|
||||||
|
SecretString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PutSecretValueHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly secretService: SecretService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.SecretsManagerPutSecretValue;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
ClientRequestToken: Joi.string(),
|
||||||
|
SecretId: Joi.string().required(),
|
||||||
|
SecretString: Joi.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const { SecretId, SecretString: secretString, ClientRequestToken } = params;
|
||||||
|
const name = Secret.getNameFromSecretId(SecretId);
|
||||||
|
const oldSecret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
||||||
|
|
||||||
|
if (!oldSecret) {
|
||||||
|
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = await this.secretService.create({
|
||||||
|
versionId: ClientRequestToken,
|
||||||
|
name: oldSecret.name,
|
||||||
|
secretString,
|
||||||
|
accountId: awsProperties.accountId,
|
||||||
|
region: awsProperties.region,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name, VersionStages: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,4 +28,9 @@ export class Secret extends BaseEntity {
|
||||||
get arn(): string {
|
get arn(): string {
|
||||||
return `arn:aws:secretsmanager:${this.region}:${this.accountId}:${this.name}`;
|
return `arn:aws:secretsmanager:${this.region}:${this.accountId}:${this.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getNameFromSecretId(secretId: string) {
|
||||||
|
const parts = secretId.split(':');
|
||||||
|
return parts.length > 1 ? parts.pop() : secretId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { CreateSecretDto } from './create-secret.dto';
|
||||||
|
import { Secret } from './secret.entity';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SecretService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Secret)
|
||||||
|
private readonly secretRepo: Repository<Secret>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findLatestByNameAndRegion(name: string, region: string): Promise<Secret> {
|
||||||
|
return await this.secretRepo.findOne({ where: { name, region }, order: { createdAt: 'DESC' } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByNameAndVersion(name: string, versionId: string): Promise<Secret> {
|
||||||
|
// TypeORM BUG: https://github.com/typeorm/typeorm/issues/5694 - Cannot use findOne here
|
||||||
|
const [ secret ] = await this.secretRepo.find({ where: { name, versionId } });
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateSecretDto): Promise<Secret> {
|
||||||
|
return await this.secretRepo.create({
|
||||||
|
...dto,
|
||||||
|
versionId: dto.versionId ?? uuid.v4(),
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,21 @@ import { DefaultActionHandlerProvider } from '../default-action-handler/default-
|
||||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||||
import { CreateSecretHandler } from './create-secret.handler';
|
import { CreateSecretHandler } from './create-secret.handler';
|
||||||
import { DescribeSecretHandler } from './describe-secret.handler';
|
import { DescribeSecretHandler } from './describe-secret.handler';
|
||||||
|
import { GetResourcePolicyHandler } from './get-resource-policy.handler';
|
||||||
|
import { GetSecretValueHandler } from './get-secret-value.handler';
|
||||||
|
import { PutResourcePolicyHandler } from './put-resource-policy.handler';
|
||||||
|
import { PutSecretValueHandler } from './put-secret-value.handler';
|
||||||
import { Secret } from './secret.entity';
|
import { Secret } from './secret.entity';
|
||||||
|
import { SecretService } from './secret.service';
|
||||||
import { SecretsManagerHandlers } from './secrets-manager.constants';
|
import { SecretsManagerHandlers } from './secrets-manager.constants';
|
||||||
|
|
||||||
const handlers = [
|
const handlers = [
|
||||||
CreateSecretHandler,
|
CreateSecretHandler,
|
||||||
DescribeSecretHandler,
|
DescribeSecretHandler,
|
||||||
|
GetResourcePolicyHandler,
|
||||||
|
GetSecretValueHandler,
|
||||||
|
PutResourcePolicyHandler,
|
||||||
|
PutSecretValueHandler,
|
||||||
]
|
]
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
|
|
@ -46,6 +55,7 @@ const actions = [
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
SecretService,
|
||||||
...handlers,
|
...handlers,
|
||||||
ExistingActionHandlersProvider(handlers),
|
ExistingActionHandlersProvider(handlers),
|
||||||
DefaultActionHandlerProvider(SecretsManagerHandlers, Format.Json, actions),
|
DefaultActionHandlerProvider(SecretsManagerHandlers, Format.Json, actions),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { BadRequestException, 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 { SqsQueueEntryService } from '../sqs/sqs-queue-entry.service';
|
||||||
|
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
import { SqsQueue } from '../sqs/sqs-queue.entity';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
TopicArn: string;
|
||||||
|
TargetArn: string;
|
||||||
|
Subject?: string;
|
||||||
|
Message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PublishHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SnsTopicSubscription)
|
||||||
|
private readonly snsTopicSubscriptionRepo: Repository<SnsTopicSubscription>,
|
||||||
|
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SnsPublish;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
TargetArn: Joi.string(),
|
||||||
|
TopicArn: Joi.string(),
|
||||||
|
Subject: Joi.string().allow(null, ''),
|
||||||
|
Message: Joi.string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ TopicArn, TargetArn, Message, Subject }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
const arn = TopicArn ?? TargetArn;
|
||||||
|
|
||||||
|
if (!arn) {
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageId = uuid.v4();
|
||||||
|
const subscriptions = await this.snsTopicSubscriptionRepo.find({ where: { topicArn: arn } });
|
||||||
|
const topicAttributes = await this.attributeService.getByArn(arn);
|
||||||
|
|
||||||
|
for (const sub of subscriptions) {
|
||||||
|
const attributes = await this.attributeService.getByArn(sub.arn);
|
||||||
|
if (sub.protocol === 'sqs') {
|
||||||
|
const { value: isRaw } = attributes.find(a => a.name === 'RawMessageDelivery');
|
||||||
|
const [queueAccountId, queueName] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(sub.endpoint);
|
||||||
|
|
||||||
|
const message = isRaw === 'true' ? Message : JSON.stringify({
|
||||||
|
Type: "Notification",
|
||||||
|
MessageId,
|
||||||
|
TopicArn: arn,
|
||||||
|
Subject,
|
||||||
|
Message,
|
||||||
|
Timestamp: new Date().toISOString(),
|
||||||
|
SignatureVersion: topicAttributes.find(a => a.name === 'SignatureVersion')?.value ?? '1',
|
||||||
|
Signature: '',
|
||||||
|
SigningCertURL: '',
|
||||||
|
UnsubscribeURL: `${awsProperties.host}/?Action=Unsubscribe&SubscriptionArn=${sub.arn}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.sqsQueueEntryService.publish(queueAccountId, queueName, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { MessageId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,13 @@ import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entit
|
||||||
import { ExistingActionHandlers } from '../default-action-handler/default-action-handler.constants';
|
import { ExistingActionHandlers } from '../default-action-handler/default-action-handler.constants';
|
||||||
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||||
|
import { SqsModule } from '../sqs/sqs.module';
|
||||||
import { CreateTopicHandler } from './create-topic.handler';
|
import { CreateTopicHandler } from './create-topic.handler';
|
||||||
import { GetSubscriptionAttributesHandler } from './get-subscription-attributes.handler';
|
import { GetSubscriptionAttributesHandler } from './get-subscription-attributes.handler';
|
||||||
import { GetTopicAttributesHandler } from './get-topic-attributes.handler';
|
import { GetTopicAttributesHandler } from './get-topic-attributes.handler';
|
||||||
import { ListTagsForResourceHandler } from './list-tags-for-resource.handler';
|
import { ListTagsForResourceHandler } from './list-tags-for-resource.handler';
|
||||||
import { ListTopicsHandler } from './list-topics.handler';
|
import { ListTopicsHandler } from './list-topics.handler';
|
||||||
|
import { PublishHandler } from './publish.handler';
|
||||||
import { SetSubscriptionAttributesHandler } from './set-subscription-attributes.handler';
|
import { SetSubscriptionAttributesHandler } from './set-subscription-attributes.handler';
|
||||||
import { SetTopicAttributesHandler } from './set-topic-attributes.handler';
|
import { SetTopicAttributesHandler } from './set-topic-attributes.handler';
|
||||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
||||||
|
|
@ -24,6 +26,7 @@ const handlers = [
|
||||||
GetTopicAttributesHandler,
|
GetTopicAttributesHandler,
|
||||||
ListTagsForResourceHandler,
|
ListTagsForResourceHandler,
|
||||||
ListTopicsHandler,
|
ListTopicsHandler,
|
||||||
|
PublishHandler,
|
||||||
SetSubscriptionAttributesHandler,
|
SetSubscriptionAttributesHandler,
|
||||||
SetTopicAttributesHandler,
|
SetTopicAttributesHandler,
|
||||||
SubscribeHandler,
|
SubscribeHandler,
|
||||||
|
|
@ -78,6 +81,7 @@ const actions = [
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([SnsTopic, SnsTopicSubscription]),
|
TypeOrmModule.forFeature([SnsTopic, SnsTopicSubscription]),
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
|
SqsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
...handlers,
|
...handlers,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
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 { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
QueueName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateQueueHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SqsQueue)
|
||||||
|
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
||||||
|
private readonly tagsService: TagsService,
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SqsCreateQueue;
|
||||||
|
validator = Joi.object<QueryParams, true>({ QueueName: Joi.string().required() });
|
||||||
|
|
||||||
|
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const { QueueName: name } = params;
|
||||||
|
|
||||||
|
const queue = await this.sqsQueueRepo.create({
|
||||||
|
name,
|
||||||
|
accountId: awsProperties.accountId,
|
||||||
|
region: awsProperties.region,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
const tags = TagsService.tagPairs(params);
|
||||||
|
await this.tagsService.createMany(queue.arn, tags);
|
||||||
|
|
||||||
|
const attributes = AttributesService.attributePairs(params);
|
||||||
|
await this.attributeService.createMany(queue.arn, attributes);
|
||||||
|
|
||||||
|
return { QueueUrl: queue.getUrl(awsProperties.host) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
__path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PurgeQueueHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SqsPurgeQueue;
|
||||||
|
validator = Joi.object<QueryParams, true>({ __path: Joi.string().required() });
|
||||||
|
|
||||||
|
protected async handle({ __path }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(__path);
|
||||||
|
await this.sqsQueueEntryService.purge(accountId, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
__path: string;
|
||||||
|
MaxNumberOfMessages?: number;
|
||||||
|
VisibilityTimeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ReceiveMessageHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SqsReceiveMessage;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
__path: Joi.string().required(),
|
||||||
|
MaxNumberOfMessages: Joi.number(),
|
||||||
|
VisibilityTimeout: Joi.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ __path, MaxNumberOfMessages, VisibilityTimeout }: QueryParams, awsProperties: AwsProperties) {
|
||||||
|
|
||||||
|
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(__path);
|
||||||
|
const records = await this.sqsQueueEntryService.recieveMessages(accountId, name, MaxNumberOfMessages, VisibilityTimeout);
|
||||||
|
return records.map(r => ({
|
||||||
|
Message: {
|
||||||
|
MessageId: r.id,
|
||||||
|
ReceiptHandle: r.id,
|
||||||
|
MD5OfBody: crypto.createHash('md5').update(r.message).digest("hex"),
|
||||||
|
Body: r.message,
|
||||||
|
'#': [
|
||||||
|
{ Attribute: { Name: 'SenderId', Value: r.senderId }},
|
||||||
|
{ Attribute: { Name: 'SentTimestamp', Value: r.createdAt.getSeconds() }},
|
||||||
|
{ Attribute: { Name: 'ApproximateReceiveCount', Value: 1 }},
|
||||||
|
{ Attribute: { Name: 'ApproximateFirstReceiveTimestamp', Value: r.createdAt.getSeconds() }},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
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 { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
'Attribute.Name': string;
|
||||||
|
'Attribute.Value': string;
|
||||||
|
__path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SetQueueAttributesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SqsQueue)
|
||||||
|
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
||||||
|
private readonly attributeService: AttributesService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.SqsSetQueueAttributes;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
'Attribute.Name': Joi.string().required(),
|
||||||
|
'Attribute.Value': Joi.string().required(),
|
||||||
|
__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 } });
|
||||||
|
|
||||||
|
if(!queue) {
|
||||||
|
throw new BadRequestException('ResourceNotFoundException');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.attributeService.create({ name: params['Attribute.Name'], value: params['Attribute.Value'], arn: queue.arn });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
|
||||||
|
type QueueEntry = {
|
||||||
|
id: string;
|
||||||
|
queueArn: string;
|
||||||
|
senderId: string;
|
||||||
|
message: string;
|
||||||
|
inFlightReleaseDate: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SqsQueueEntryService {
|
||||||
|
|
||||||
|
// Heavy use may require event-driven locking implementation
|
||||||
|
private queues: Record<string, QueueEntry[]> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SqsQueue)
|
||||||
|
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async publish(accountId: string, queueName: string, message: string) {
|
||||||
|
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
||||||
|
|
||||||
|
if (!queue) {
|
||||||
|
console.warn(`Warning bad subscription to ${queueName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.queues) {
|
||||||
|
this.queues[queue.arn] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queues[queue.arn].push({
|
||||||
|
id: uuid.v4(),
|
||||||
|
queueArn: queue.arn,
|
||||||
|
senderId: accountId,
|
||||||
|
message,
|
||||||
|
inFlightReleaseDate: new Date(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async recieveMessages(accountId: string, queueName: string, maxNumberOfMessages = 10, visabilityTimeout = 0): Promise<QueueEntry[]> {
|
||||||
|
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
||||||
|
|
||||||
|
if (!queue) {
|
||||||
|
throw new BadRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessDate = new Date();
|
||||||
|
const newInFlightReleaseDate = new Date(accessDate);
|
||||||
|
newInFlightReleaseDate.setSeconds(accessDate.getSeconds() + visabilityTimeout);
|
||||||
|
const records = this.queues[queue.arn]?.filter(e => e.inFlightReleaseDate <= accessDate).slice(0, maxNumberOfMessages - 1);
|
||||||
|
records.forEach(e => e.inFlightReleaseDate = newInFlightReleaseDate);
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
async purge(accountId: string, queueName: string) {
|
||||||
|
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
||||||
|
this.queues[queue.arn] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('sqs_queue')
|
||||||
|
export class SqsQueue extends BaseEntity {
|
||||||
|
|
||||||
|
@PrimaryColumn({ name: 'name' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ name: 'account_id', nullable: false })
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'region', nullable: false })
|
||||||
|
region: string;
|
||||||
|
|
||||||
|
get arn(): string {
|
||||||
|
return `arn:aws:sns:${this.region}:${this.accountId}:${this.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl(host: string): string {
|
||||||
|
return `${host}/${this.accountId}/${this.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAccountIdAndNameFromPath(path: string): [string, string] {
|
||||||
|
const [_, accountId, name] = path.split('/');
|
||||||
|
return [accountId, name];
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAccountIdAndNameFromArn(arn: string): [string, string] {
|
||||||
|
const parts = arn.split(':');
|
||||||
|
const name = parts.pop();
|
||||||
|
const accountId = parts.pop();
|
||||||
|
return [accountId, name];
|
||||||
|
}
|
||||||
|
|
||||||
|
static tryGetAccountIdAndNameFromPathOrArn(pathOrArn: string): [string, string] {
|
||||||
|
if (pathOrArn.split(':').length) {
|
||||||
|
return SqsQueue.getAccountIdAndNameFromArn(pathOrArn);
|
||||||
|
}
|
||||||
|
return SqsQueue.getAccountIdAndNameFromPath(pathOrArn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AbstractActionHandler } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
|
||||||
|
export type SqsHandlers = Record<Action, AbstractActionHandler>;
|
||||||
|
export const SqsHandlers = Symbol.for('SQS_HANDLERS');
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
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 { CreateQueueHandler } from './create-queue.handler';
|
||||||
|
import { PurgeQueueHandler } from './purge-queue.handler';
|
||||||
|
import { SetQueueAttributesHandler } from './set-queue-attributes.handler';
|
||||||
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import { SqsHandlers } from './sqs.constants';
|
||||||
|
|
||||||
|
const handlers = [
|
||||||
|
CreateQueueHandler,
|
||||||
|
PurgeQueueHandler,
|
||||||
|
SetQueueAttributesHandler,
|
||||||
|
]
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
Action.SqsAddPermisson,
|
||||||
|
Action.SqsChangeMessageVisibility,
|
||||||
|
Action.SqsChangeMessageVisibilityBatch,
|
||||||
|
Action.SqsCreateQueue,
|
||||||
|
Action.SqsDeleteMessage,
|
||||||
|
Action.SqsDeleteMessageBatch,
|
||||||
|
Action.SqsDeleteQueue,
|
||||||
|
Action.SqsGetQueueAttributes,
|
||||||
|
Action.SqsGetQueueUrl,
|
||||||
|
Action.SqsListDeadLetterSourceQueues,
|
||||||
|
Action.SqsListQueues,
|
||||||
|
Action.SqsListQueueTags,
|
||||||
|
Action.SqsPurgeQueue,
|
||||||
|
Action.SqsReceiveMessage,
|
||||||
|
Action.SqsRemovePermission,
|
||||||
|
Action.SqsSendMessage,
|
||||||
|
Action.SqsSendMessageBatch,
|
||||||
|
Action.SqsSetQueueAttributes,
|
||||||
|
Action.SqsTagQueue,
|
||||||
|
Action.SqsUntagQueue,
|
||||||
|
]
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([SqsQueue]),
|
||||||
|
AwsSharedEntitiesModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
...handlers,
|
||||||
|
SqsQueueEntryService,
|
||||||
|
ExistingActionHandlersProvider(handlers),
|
||||||
|
DefaultActionHandlerProvider(SqsHandlers, Format.Xml, actions),
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
SqsHandlers,
|
||||||
|
SqsQueueEntryService,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SqsModule {}
|
||||||
57
yarn.lock
57
yarn.lock
|
|
@ -301,6 +301,21 @@
|
||||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||||
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
|
||||||
|
|
||||||
|
"@types/body-parser@*":
|
||||||
|
version "1.19.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
||||||
|
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
|
||||||
|
dependencies:
|
||||||
|
"@types/connect" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/connect@*":
|
||||||
|
version "3.4.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
|
||||||
|
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/eslint-scope@^3.7.3":
|
"@types/eslint-scope@^3.7.3":
|
||||||
version "3.7.4"
|
version "3.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
||||||
|
|
@ -327,11 +342,35 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
|
||||||
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
||||||
|
|
||||||
|
"@types/express-serve-static-core@^4.17.33":
|
||||||
|
version "4.17.33"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543"
|
||||||
|
integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/range-parser" "*"
|
||||||
|
|
||||||
|
"@types/express@^4.17.17":
|
||||||
|
version "4.17.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
|
||||||
|
integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/body-parser" "*"
|
||||||
|
"@types/express-serve-static-core" "^4.17.33"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/serve-static" "*"
|
||||||
|
|
||||||
"@types/json-schema@*", "@types/json-schema@^7.0.8":
|
"@types/json-schema@*", "@types/json-schema@^7.0.8":
|
||||||
version "7.0.11"
|
version "7.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||||
|
|
||||||
|
"@types/mime@*":
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
|
||||||
|
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "18.15.3"
|
version "18.15.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014"
|
||||||
|
|
@ -342,6 +381,24 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
|
"@types/qs@*":
|
||||||
|
version "6.9.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||||
|
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
|
||||||
|
|
||||||
|
"@types/range-parser@*":
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||||
|
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||||
|
|
||||||
|
"@types/serve-static@*":
|
||||||
|
version "1.15.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d"
|
||||||
|
integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/mime" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/uuid@8.3.4":
|
"@types/uuid@8.3.4":
|
||||||
version "8.3.4"
|
version "8.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue