More refactors, adding wider support

This commit is contained in:
Matthew Bessette 2025-01-17 00:01:59 -05:00
parent da84b6b085
commit d8930a6a30
78 changed files with 711 additions and 361 deletions

View File

@ -33,5 +33,5 @@ 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
* handle(queryParams: T, { awsProperties} : RequestContext): Record<string, any> | void
* the method that implements the AWS action

View File

@ -1,93 +0,0 @@
-- CreateTable
CREATE TABLE "Attribute" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"arn" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "Audit" (
"id" TEXT NOT NULL PRIMARY KEY,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"action" TEXT,
"request" TEXT,
"response" TEXT
);
-- CreateTable
CREATE TABLE "Secret" (
"versionId" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"secretString" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deletionDate" DATETIME
);
-- CreateTable
CREATE TABLE "SnsTopic" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "SnsTopicSubscription" (
"id" TEXT NOT NULL PRIMARY KEY,
"topicArn" TEXT NOT NULL,
"endpoint" TEXT,
"protocol" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "SqsQueue" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "SqsQueueMessage" (
"id" TEXT NOT NULL PRIMARY KEY,
"queueId" INTEGER NOT NULL,
"senderId" TEXT NOT NULL,
"message" TEXT NOT NULL,
"inFlightRelease" DATETIME NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SqsQueueMessage_queueId_fkey" FOREIGN KEY ("queueId") REFERENCES "SqsQueue" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Tag" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"arn" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Attribute_arn_name_key" ON "Attribute"("arn", "name");
-- CreateIndex
CREATE INDEX "Secret_name_idx" ON "Secret"("name");
-- CreateIndex
CREATE UNIQUE INDEX "SnsTopic_accountId_region_name_key" ON "SnsTopic"("accountId", "region", "name");
-- CreateIndex
CREATE UNIQUE INDEX "SqsQueue_accountId_region_name_key" ON "SqsQueue"("accountId", "region", "name");
-- CreateIndex
CREATE INDEX "SqsQueueMessage_queueId_idx" ON "SqsQueueMessage"("queueId");
-- CreateIndex
CREATE UNIQUE INDEX "Tag_arn_name_key" ON "Tag"("arn", "name");

View File

@ -1,21 +0,0 @@
-- CreateTable
CREATE TABLE "KmsAlias" (
"name" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"kmsKeyId" TEXT NOT NULL,
PRIMARY KEY ("accountId", "region", "name")
);
-- CreateTable
CREATE TABLE "KmsKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"usage" TEXT NOT NULL,
"description" TEXT NOT NULL,
"keySpec" TEXT NOT NULL,
"key" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@ -1,25 +0,0 @@
/*
Warnings:
- Added the required column `updatedAt` to the `KmsAlias` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_KmsAlias" (
"name" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"kmsKeyId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
PRIMARY KEY ("accountId", "region", "name"),
CONSTRAINT "KmsAlias_kmsKeyId_fkey" FOREIGN KEY ("kmsKeyId") REFERENCES "KmsKey" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_KmsAlias" ("accountId", "kmsKeyId", "name", "region") SELECT "accountId", "kmsKeyId", "name", "region" FROM "KmsAlias";
DROP TABLE "KmsAlias";
ALTER TABLE "new_KmsAlias" RENAME TO "KmsAlias";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -1,36 +0,0 @@
/*
Warnings:
- You are about to alter the column `key` on the `KmsKey` table. The data in that column could be lost. The data in that column will be cast from `String` to `Binary`.
- Added the required column `enabled` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `keyState` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `multiRegion` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `origin` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `policy` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `KmsKey` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_KmsKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"enabled" BOOLEAN NOT NULL,
"usage" TEXT NOT NULL,
"description" TEXT NOT NULL,
"keySpec" TEXT NOT NULL,
"keyState" TEXT NOT NULL,
"origin" TEXT NOT NULL,
"multiRegion" BOOLEAN NOT NULL,
"policy" TEXT NOT NULL,
"key" BLOB NOT NULL,
"accountId" TEXT NOT NULL,
"region" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_KmsKey" ("accountId", "createdAt", "description", "id", "key", "keySpec", "region", "usage") SELECT "accountId", "createdAt", "description", "id", "key", "keySpec", "region", "usage" FROM "KmsKey";
DROP TABLE "KmsKey";
ALTER TABLE "new_KmsKey" RENAME TO "KmsKey";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@ -1,3 +0,0 @@
-- AlterTable
ALTER TABLE "KmsKey" ADD COLUMN "nextRotation" DATETIME;
ALTER TABLE "KmsKey" ADD COLUMN "rotationPeriod" INTEGER;

View File

@ -37,12 +37,14 @@ model IamRole {
accountId String
createdAt DateTime @default(now())
policies IamRoleIamPolicyAttachment[]
@@unique([accountId, name])
}
model IamPolicy {
id String
version Int @default(1)
version Int @default(1)
isDefault Boolean
path String?
name String
@ -57,6 +59,15 @@ model IamPolicy {
@@unique([accountId, path, name])
}
model IamRoleIamPolicyAttachment {
iamRoleId String
iamPolicyId String
role IamRole @relation(fields: [iamRoleId], references: [id])
@@id([iamRoleId, iamPolicyId])
}
model KmsAlias {
name String
accountId String

View File

@ -1,11 +1,12 @@
import { Request } from "express";
import { Action } from "../action.enum";
import { Format } from "../abstract-action.handler";
import { AwsProperties, Format } from "../abstract-action.handler";
export interface RequestContext {
action?: Action;
format?: Format;
awsProperties: AwsProperties;
readonly requestId: string;
}

View File

@ -1,6 +1,7 @@
import { randomUUID } from 'crypto';
import { Action } from './action.enum';
import * as Joi from 'joi';
import { RequestContext } from './_context/request.context';
export type AwsProperties = {
accountId: string;
@ -17,18 +18,18 @@ export abstract class AbstractActionHandler<T = Record<string, string | number |
audit = true;
abstract format: Format;
abstract action: Action;
abstract action: Action | Action[];
abstract validator: Joi.ObjectSchema<T>;
protected abstract handle(queryParams: T, awsProperties: AwsProperties): Record<string, any> | void;
protected abstract handle(queryParams: T, context: RequestContext): Record<string, any> | void;
async getResponse(queryParams: T, awsProperties: AwsProperties) {
async getResponse(queryParams: T, context: RequestContext) {
if (this.format === Format.Xml) {
return await this.getXmlResponse(queryParams, awsProperties);
return await this.getXmlResponse(queryParams, context);
}
return await this.getJsonResponse(queryParams, awsProperties);
return await this.getJsonResponse(queryParams, context);
}
private async getXmlResponse(queryParams: T, awsProperties: AwsProperties) {
private async getXmlResponse(queryParams: T, context: RequestContext) {
const response = {
'@': {
xmlns: "https://sns.amazonaws.com/doc/2010-03-31/"
@ -38,21 +39,22 @@ export abstract class AbstractActionHandler<T = Record<string, string | number |
}
}
const result = await this.handle(queryParams, awsProperties);
const result = await this.handle(queryParams, context);
if (!result) {
return response;
}
const action = Array.isArray(this.action) ? this.action[0] : this.action;
return {
[`${this.action}Result`]: {
[`${action}Result`]: {
...result,
}
}
}
private async getJsonResponse(queryParams: T, awsProperties: AwsProperties) {
const result = await this.handle(queryParams, awsProperties);
private async getJsonResponse(queryParams: T, context: RequestContext) {
const result = await this.handle(queryParams, context);
if (result) {
return result;
}

View File

@ -302,6 +302,28 @@ export enum Action {
SqsTagQueue = 'TagQueue',
SqsUntagQueue = 'UntagQueue',
// V2 SQS
V2_SqsAddPermisson = 'AmazonSQS.AddPermission',
V2_SqsChangeMessageVisibility = 'AmazonSQS.ChangeMessageVisibility',
V2_SqsChangeMessageVisibilityBatch = 'AmazonSQS.ChangeMessageVisibilityBatch',
V2_SqsCreateQueue = 'AmazonSQS.CreateQueue',
V2_SqsDeleteMessage = 'AmazonSQS.DeleteMessage',
V2_SqsDeleteMessageBatch = 'AmazonSQS.DeleteMessageBatch',
V2_SqsDeleteQueue = 'AmazonSQS.DeleteQueue',
V2_SqsGetQueueAttributes = 'AmazonSQS.GetQueueAttributes',
V2_SqsGetQueueUrl = 'AmazonSQS.GetQueueUrl',
V2_SqsListDeadLetterSourceQueues = 'AmazonSQS.ListDeadLetterSourceQueues',
V2_SqsListQueues = 'AmazonSQS.ListQueues',
V2_SqsListQueueTags = 'AmazonSQS.ListQueueTags',
V2_SqsPurgeQueue = 'AmazonSQS.PurgeQueue',
V2_SqsReceiveMessage = 'AmazonSQS.ReceiveMessage',
V2_SqsRemovePermission = 'AmazonSQS.RemovePermission',
V2_SqsSendMessage = 'AmazonSQS.SendMessage',
V2_SqsSendMessageBatch = 'AmazonSQS.SendMessageBatch',
V2_SqsSetQueueAttributes = 'AmazonSQS.SetQueueAttributes',
V2_SqsTagQueue = 'AmazonSQS.TagQueue',
V2_SqsUntagQueue = 'AmazonSQS.UntagQueue',
// STS
StsAssumeRole = 'AssumeRole',
StsAssumeRoleWithSaml = 'AssumeRoleWithSaml',

View File

@ -7,9 +7,10 @@ import * as js2xmlparser from 'js2xmlparser';
import { AbstractActionHandler, Format } from './abstract-action.handler';
import { Action } from './action.enum';
import { ActionHandlers } from './app.constants';
import { AuditInterceptor } from './_context/audit.interceptor';
import { AuditInterceptor } from './audit/audit.interceptor';
import { CommonConfig } from './config/common-config.interface';
import { InvalidAction, ValidationError } from './aws-shared-entities/aws-exceptions';
import { IRequest } from './_context/request.context';
type QueryParams = {
__path: string;
@ -28,7 +29,7 @@ export class AppController {
@HttpCode(200)
@UseInterceptors(AuditInterceptor)
async post(
@Req() request: Request,
@Req() request: IRequest,
@Body() body: Record<string, any>,
@Headers() headers: Record<string, any>,
) {
@ -56,15 +57,11 @@ export class AppController {
throw new ValidationError(validatorError.message);
}
const awsProperties = {
accountId: this.configService.get('AWS_ACCOUNT_ID'),
region: this.configService.get('AWS_REGION'),
host: `${this.configService.get('PROTO')}://${this.configService.get('HOST')}:${this.configService.get('PORT')}`,
};
const jsonResponse = await handler.getResponse(validQueryParams, request.context);
const jsonResponse = await handler.getResponse(validQueryParams, awsProperties);
if (handler.format === Format.Xml) {
return js2xmlparser.parse(`${handler.action}Response`, jsonResponse);
const action = Array.isArray(handler.action) ? handler.action[0] : handler.action;
return js2xmlparser.parse(`${action}Response`, jsonResponse);
}
return jsonResponse;
}

View File

@ -3,7 +3,7 @@ import { ConfigModule } from '@nestjs/config';
import { ActionHandlers } from './app.constants';
import { AppController } from './app.controller';
import { AuditInterceptor } from './_context/audit.interceptor';
import { AuditInterceptor } from './audit/audit.interceptor';
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
import localConfig from './config/local.config';
import { KMSHandlers } from './kms/kms.constants';

View File

@ -0,0 +1,11 @@
import { Controller } from "@nestjs/common";
import { AuditService } from "./audit.service";
@Controller('_audit')
export class AuditController {
constructor(
private readonly auditService: AuditService,
) {}
}

View File

@ -9,7 +9,8 @@ import { ActionHandlers } from '../app.constants';
import { Action } from '../action.enum';
import { Format } from '../abstract-action.handler';
import { AwsException, InternalFailure } from '../aws-shared-entities/aws-exceptions';
import { IRequest, RequestContext } from './request.context';
import { IRequest, RequestContext } from '../_context/request.context';
import { ConfigService } from '@nestjs/config';
@Injectable()
@ -21,12 +22,20 @@ export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
@Inject(ActionHandlers)
private readonly handlers: ActionHandlers,
private readonly prismaService: PrismaService,
private readonly configService: ConfigService,
) {}
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
const awsProperties = {
accountId: this.configService.get('AWS_ACCOUNT_ID'),
region: this.configService.get('AWS_REGION'),
host: `${this.configService.get('PROTO')}://${this.configService.get('HOST')}:${this.configService.get('PORT')}`,
};
const requestContext: RequestContext = {
requestId: randomUUID(),
awsProperties,
}
const httpContext = context.switchToHttp();

12
src/audit/audit.module.ts Normal file
View File

@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { PrismaModule } from "../_prisma/prisma.module";
import { AuditController } from "./audit.controller";
import { AuditInterceptor } from "./audit.interceptor";
@Module({
imports: [PrismaModule],
controllers: [AuditController],
providers: [AuditInterceptor],
})
export class AuditModule {}

View File

@ -0,0 +1,14 @@
import { Injectable } from "@nestjs/common";
import { PrismaService } from "../_prisma/prisma.service";
@Injectable()
export class AuditService {
constructor(
private readonly prismaService: PrismaService,
) {}
}

View File

@ -172,3 +172,23 @@ export class EntityAlreadyExists extends AwsException {
)
}
}
export class NoSuchEntity extends AwsException {
constructor() {
super(
'The request was rejected because it referenced a resource entity that does not exist. The error message describes the resource.',
NoSuchEntity.name,
HttpStatus.NOT_FOUND,
)
}
}
export class QueueNameExists extends AwsException {
constructor() {
super(
'A queue with this name already exists. Amazon SQS returns this error only if the request includes attributes whose values differ from those of the existing queue.',
QueueNameExists.name,
HttpStatus.BAD_REQUEST,
)
}
}

View File

@ -37,7 +37,7 @@ export class TagsService {
await this.prismaService.tag.deleteMany({ where: { arn, name } });
}
static tagPairs(queryParams: Record<string, string>): { key: string, value: string }[] {
static tagPairs(queryParams: Record<string, any>): { key: string, value: string }[] {
const pairs: { key: string, value: string }[] = [];
for (const param of Object.keys(queryParams)) {
const components = breakdownAwsQueryParam(param);

View File

@ -7,6 +7,14 @@ import { ExistingActionHandlers } from './default-action-handler.constants';
export const ExistingActionHandlersProvider = (inject: Array<InjectionToken | OptionalFactoryDependency>): Provider => ({
provide: ExistingActionHandlers,
useFactory: (...args: AbstractActionHandler[]) => args.reduce((m, h) => {
if (Array.isArray(h.action)) {
for (const action of h.action) {
m[action] = h;
}
return m;
}
m[h.action] = h;
return m;
}, {} as Record<Action, AbstractActionHandler>),

View File

@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamService } from './iam.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
PolicyArn: string;
@ -12,7 +14,7 @@ type QueryParams = {
export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
) {
super();
}
@ -24,8 +26,12 @@ export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams>
RoleName: Joi.string().required(),
});
protected async handle({ PolicyArn, RoleName }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ PolicyArn, RoleName }: QueryParams, context: RequestContext) {
await this.iamService.attachPolicyToRoleName(
context.awsProperties.accountId,
PolicyArn,
RoleName
);
}
}

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamPolicy } from './iam-policy.entity';
import { breakdownArn } from '../util/breakdown-arn';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
PolicyArn: string;
@ -27,7 +28,7 @@ export class CreatePolicyVersionHandler extends AbstractActionHandler<QueryParam
SetAsDefault: Joi.boolean().required(),
});
protected async handle({ PolicyArn, PolicyDocument, SetAsDefault }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ PolicyArn, PolicyDocument, SetAsDefault }: QueryParams, { awsProperties} : RequestContext) {

View File

@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { AbstractActionHandler, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamPolicy } from './iam-policy.entity';
import { IamService } from './iam.service';
import { TagsService } from '../aws-shared-entities/tags.service';
import { randomUUID } from 'crypto';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
Description: string;
@ -33,7 +33,7 @@ export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
PolicyName: Joi.string().min(1).max(128).required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams, context: RequestContext) {
const { Description, Path, PolicyName, PolicyDocument } = params;
@ -45,9 +45,11 @@ export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
path: Path,
description: Description,
policy: PolicyDocument,
accountId: awsProperties.accountId,
accountId: context.awsProperties.accountId,
});
return policy.metadata;
return {
Policy: policy.metadata
};
}
}

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamService } from './iam.service';
import { randomUUID } from 'crypto';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
RoleName: string;
@ -32,7 +33,7 @@ export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
RoleName: Joi.string().min(1).max(64).required(),
});
protected async handle({ RoleName, Path, AssumeRolePolicyDocument, MaxSessionDuration, Description }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ RoleName, Path, AssumeRolePolicyDocument, MaxSessionDuration, Description }: QueryParams, { awsProperties} : RequestContext) {
const role = await this.iamService.createRole({
id: randomUUID(),

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamService } from './iam.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
RoleName: string;
@ -24,7 +25,7 @@ export class DeleteRoleHandler extends AbstractActionHandler<QueryParams> {
RoleName: Joi.string().min(1).max(64).required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ RoleName }: QueryParams, { awsProperties} : RequestContext) {
await this.iamService.deleteRoleByName(awsProperties.accountId, RoleName);
}
}

View File

@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
import { IamService } from './iam.service';
type QueryParams = {
PolicyArn: string;
@ -12,7 +14,7 @@ type QueryParams = {
export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
) {
super();
}
@ -24,7 +26,17 @@ export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams>
VersionId: Joi.string().required(),
});
protected async handle({ PolicyArn, VersionId }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ PolicyArn, VersionId }: QueryParams, { awsProperties} : RequestContext) {
const maybeVersion = Number(VersionId);
const version = Number.isNaN(maybeVersion) ? Number(VersionId.toLowerCase().split('v')[1]) : Number(maybeVersion);
const policy = await this.iamService.getPolicyByArnAndVersion(PolicyArn, version);
return {
PolicyVersion: {
Document: policy.policy,
IsDefaultVersion: policy.isDefault,
VersionId: policy.version,
CreateDate: policy.createdAt.toISOString(),
}
}
}
}

View File

@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
import { IamService } from './iam.service';
type QueryParams = {
PolicyArn: string;
@ -11,7 +13,7 @@ type QueryParams = {
export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
) {
super();
}
@ -22,7 +24,10 @@ export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
PolicyArn: Joi.string().required(),
});
protected async handle({ PolicyArn }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ PolicyArn }: QueryParams, { awsProperties} : RequestContext) {
const policy = await this.iamService.getPolicyByArn(PolicyArn);
return {
Policy: policy.metadata,
}
}
}

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { IamService } from './iam.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
RoleName: string;
@ -24,14 +25,8 @@ export class GetRoleHandler extends AbstractActionHandler<QueryParams> {
RoleName: Joi.string().min(1).max(64).required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ RoleName }: QueryParams, { awsProperties} : RequestContext) {
const role = await this.iamService.findOneRoleByName(awsProperties.accountId, RoleName);
if (!role) {
throw new NotFoundException();
}
return {
Role: role.metadata,
}

View File

@ -10,11 +10,19 @@ import { IAMHandlers } from './iam.constants';
import { PrismaModule } from '../_prisma/prisma.module';
import { IamService } from './iam.service';
import { GetRoleHandler } from './get-role.handler';
import { GetPolicyHandler } from './get-policy.handler';
import { GetPolicyVersionHandler } from './get-policy-version.handler';
import { AttachRolePolicyHandler } from './attach-role-policy.handler';
import { ListAttachedRolePoliciesHandler } from './list-attached-role-policies';
const handlers = [
AttachRolePolicyHandler,
CreatePolicyHandler,
CreateRoleHandler,
GetPolicyVersionHandler,
GetPolicyHandler,
GetRoleHandler,
ListAttachedRolePoliciesHandler,
]
const actions = [

View File

@ -4,7 +4,8 @@ import { PrismaService } from "../_prisma/prisma.service";
import { Prisma } from "@prisma/client";
import { IamPolicy } from "./iam-policy.entity";
import { IamRole } from "./iam-role.entity";
import { EntityAlreadyExists } from "../aws-shared-entities/aws-exceptions";
import { EntityAlreadyExists, NoSuchEntity, NotFoundException } from "../aws-shared-entities/aws-exceptions";
import { ArnUtil } from "../util/arn-util.static";
@Injectable()
export class IamService {
@ -22,15 +23,18 @@ export class IamService {
}
}
async findOneRoleByName(accountId: string, name: string): Promise<IamRole | null> {
const record = await this.prismaService.iamRole.findFirst({
where: {
name,
accountId,
}
});
return record ? new IamRole(record) : null;
async findOneRoleByName(accountId: string, name: string): Promise<IamRole> {
try {
const record = await this.prismaService.iamRole.findFirstOrThrow({
where: {
name,
accountId,
}
});
return new IamRole(record);
} catch (error) {
throw new NotFoundException();
}
}
async deleteRoleByName(accountId: string, name: string) {
@ -42,8 +46,84 @@ export class IamService {
});
}
async createPolicy(data: Prisma.IamPolicyCreateInput): Promise<IamPolicy> {
const record = await this.prismaService.iamPolicy.create({ data });
return new IamPolicy(record);
async listRolePolicies(): Promise<IamPolicy[]> {
// return await this.prismaService;
return [];
}
async getPolicyByArn(arn: string): Promise<IamPolicy> {
try {
const name = arn.split('/')[1];
const record = await this.prismaService.iamPolicy.findFirstOrThrow({
where: {
name,
},
orderBy: {
version: 'desc',
},
});
return new IamPolicy(record);
} catch (err) {
throw new NoSuchEntity();
}
}
async getPolicyByArnAndVersion(arn: string, version: number): Promise<IamPolicy> {
try {
const name = arn.split('/')[1];
const record = await this.prismaService.iamPolicy.findFirstOrThrow({
where: {
name,
version,
}
});
return new IamPolicy(record);
} catch (err) {
throw new NoSuchEntity();
}
}
async createPolicy(data: Prisma.IamPolicyCreateInput): Promise<IamPolicy> {
try {
const record = await this.prismaService.iamPolicy.create({ data });
return new IamPolicy(record);
} catch (err) {
throw new EntityAlreadyExists(`PolicyName ${data.name} already exists`);
}
}
async attachPolicyToRoleName(accountId: string, arn: string, roleName: string) {
const policy = await this.getPolicyByArn(arn);
const role = await this.findOneRoleByName(accountId, roleName);
await this.prismaService.iamRoleIamPolicyAttachment.create({
data: {
iamPolicyId: policy.id,
iamRoleId: role.id,
}
});
}
async findAttachedRolePoliciesByRoleName(accountId: string, roleName: string): Promise<IamPolicy[]> {
try {
const record = await this.prismaService.iamRole.findFirstOrThrow({
where: {
name: roleName,
accountId,
},
include: {
policies: true,
}
});
const policyIds = record.policies.map(p => p.iamPolicyId);
const policies = await this.prismaService.iamPolicy.findMany({ where: {
id: {
in: policyIds,
},
isDefault: true,
}});
return policies.map(p => new IamPolicy(p));
} catch (error) {
throw new NotFoundException();
}
}
}

View File

@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
import { IamService } from './iam.service';
type QueryParams = {
RoleName: string;
@ -11,6 +13,7 @@ type QueryParams = {
export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly iamService: IamService,
) {
super();
}
@ -21,8 +24,15 @@ export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<Query
RoleName: Joi.string().required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ RoleName }: QueryParams, { awsProperties} : RequestContext) {
const policies = await this.iamService.findAttachedRolePoliciesByRoleName(awsProperties.accountId, RoleName);
return {
AttachedPolicies: policies.map(p => ({
member: {
PolicyName: p.name,
PolicyArn: p.arn,
}
})),
}
}
}

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
Marker: string;
@ -25,7 +26,8 @@ export class ListRolePoliciesHandler extends AbstractActionHandler<QueryParams>
RoleName: Joi.string().required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ RoleName }: QueryParams, { awsProperties} : RequestContext) {
}
}

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
AliasName: string;
@ -26,7 +27,7 @@ export class CreateAliasHandler extends AbstractActionHandler<QueryParams> {
AliasName: Joi.string().min(1).max(256).regex(new RegExp(`^alias/[a-zA-Z0-9/_-]+$`)).required(),
});
protected async handle({ TargetKeyId, AliasName }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ TargetKeyId, AliasName }: QueryParams, { awsProperties} : RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(TargetKeyId, awsProperties);

View File

@ -9,6 +9,7 @@ import * as crypto from 'crypto';
import { keySpecToUsageType } from './kms-key.entity';
import { UnsupportedOperationException } from '../aws-shared-entities/aws-exceptions';
import { TagsService } from '../aws-shared-entities/tags.service';
import { RequestContext } from '../_context/request.context';
type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
@ -71,7 +72,7 @@ export class CreateKeyHandler extends AbstractActionHandler<QueryParams> {
}) as unknown as Joi.StringSchema,
});
protected async handle({ KeyUsage, Description, KeySpec, Origin, MultiRegion, Policy, Tags, CustomerMasterKeySpec }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ KeyUsage, Description, KeySpec, Origin, MultiRegion, Policy, Tags, CustomerMasterKeySpec }: QueryParams, { awsProperties} : RequestContext) {
const keySpec = CustomerMasterKeySpec ?? KeySpec;

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
GrantTokens?: string[];
@ -26,7 +27,7 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
GrantTokens: Joi.array().items(Joi.string()),
});
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ KeyId }: QueryParams, { awsProperties} : RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
KeyId: string;
@ -26,9 +27,9 @@ export class EnableKeyRotationHandler extends AbstractActionHandler<QueryParams>
RotationPeriodInDays: Joi.number().min(90).max(2560).default(365),
});
protected async handle({ KeyId, RotationPeriodInDays }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ KeyId, RotationPeriodInDays }: QueryParams, context: RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
const keyRecord = await this.kmsService.findOneByRef(KeyId, context.awsProperties);
if (!keyRecord) {
throw new NotFoundException();

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
PolicyName: string;
@ -26,9 +27,9 @@ export class GetKeyPolicyHandler extends AbstractActionHandler<QueryParams> {
PolicyName: Joi.string().min(1).max(128).default('default'),
});
protected async handle({ KeyId, PolicyName }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ KeyId, PolicyName }: QueryParams, context: RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
const keyRecord = await this.kmsService.findOneByRef(KeyId, context.awsProperties);
if (!keyRecord) {
throw new NotFoundException();

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
KeyId: string;
@ -24,7 +25,7 @@ export class GetKeyRotationStatusHandler extends AbstractActionHandler<QueryPara
KeyId: Joi.string().required(),
});
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ KeyId }: QueryParams, { awsProperties} : RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);

View File

@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
KeyId: string;
}
@Injectable()
export class GetPublicKeyHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsGetPublicKey;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
});
protected async handle({ KeyId }: QueryParams, { awsProperties} : RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
if (!keyRecord) {
throw new NotFoundException();
}
return {
...keyRecord.metadata,
PublicKey: Buffer.from(keyRecord.keyPair.publicKey).toString('base64'),
}
}
}

View File

@ -58,6 +58,10 @@ export class KmsKey implements PrismaKmsKey {
get arn() {
return `arn:aws:kms:${this.region}:${this.accountId}:key/${this.id}`;
}
get keyPair(): { publicKey: string; privateKey: string } {
return JSON.parse(Buffer.from(this.key).toString('utf-8'));
}
get metadata() {

View File

@ -16,6 +16,8 @@ import { GetKeyRotationStatusHandler } from './get-key-rotation-status.handler';
import { GetKeyPolicyHandler } from './get-key-policy.handler';
import { ListResourceTagsHandler } from './list-resource-tags.handler';
import { CreateAliasHandler } from './create-alias.handler';
import { GetPublicKeyHandler } from './get-public-key.handler';
import { SignHandler } from './sign.handler';
const handlers = [
CreateAliasHandler,
@ -24,8 +26,10 @@ const handlers = [
EnableKeyRotationHandler,
GetKeyPolicyHandler,
GetKeyRotationStatusHandler,
GetPublicKeyHandler,
ListAliasesHandler,
ListResourceTagsHandler,
SignHandler,
]
const actions = [

View File

@ -7,6 +7,7 @@ import { KmsKey } from './kms-key.entity';
import { KmsAlias } from './kms-alias.entity';
import { AwsProperties } from '../abstract-action.handler';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
@Injectable()
export class KmsService {

View File

@ -4,6 +4,7 @@ import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { KmsService } from './kms.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
KeyId?: string;
@ -28,7 +29,7 @@ export class ListAliasesHandler extends AbstractActionHandler<QueryParams> {
Marker: Joi.string(),
});
protected async handle({ KeyId, Limit, Marker }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ KeyId, Limit, Marker }: QueryParams, { awsProperties} : RequestContext) {
const records = await (KeyId
? this.kmsService.findAndCountAliasesByKeyId(awsProperties.accountId, awsProperties.region, Limit, KeyId, Marker)

View File

@ -5,6 +5,7 @@ import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { TagsService } from '../aws-shared-entities/tags.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
KeyId: string;
@ -30,9 +31,9 @@ export class ListResourceTagsHandler extends AbstractActionHandler<QueryParams>
Marker: Joi.string(),
});
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ KeyId }: QueryParams, context: RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
const keyRecord = await this.kmsService.findOneByRef(KeyId, context.awsProperties);
if (!keyRecord) {
throw new NotFoundException();

91
src/kms/sign.handler.ts Normal file
View File

@ -0,0 +1,91 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsService } from './kms.service';
import { NotFoundException, UnsupportedOperationException } from '../aws-shared-entities/aws-exceptions';
import * as crypto from 'crypto';
import { KeySpec, SigningAlgorithmSpec } from '@aws-sdk/client-kms';
import { KmsKey } from './kms-key.entity';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
KeyId: string;
Message: string;
MessageType: string;
SigningAlgorithm: string;
}
const signingAlgorithmToSigningFn: Record<SigningAlgorithmSpec, (base64: string, key: KmsKey) => string> = {
ECDSA_SHA_256: function (base64: string): string {
throw new Error('Function not implemented.');
},
ECDSA_SHA_384: function (base64: string): string {
throw new Error('Function not implemented.');
},
ECDSA_SHA_512: function (base64: string): string {
throw new Error('Function not implemented.');
},
RSASSA_PKCS1_V1_5_SHA_256: function (base64: string, key: KmsKey): string {
const buffer = Buffer.from(base64);
return crypto.sign('sha256WithRSAEncryption', buffer, key.keyPair.privateKey).toString('base64');
},
RSASSA_PKCS1_V1_5_SHA_384: function (base64: string): string {
throw new Error('Function not implemented.');
},
RSASSA_PKCS1_V1_5_SHA_512: function (base64: string): string {
throw new Error('Function not implemented.');
},
RSASSA_PSS_SHA_256: function (base64: string): string {
throw new Error('Function not implemented.');
},
RSASSA_PSS_SHA_384: function (base64: string): string {
throw new Error('Function not implemented.');
},
RSASSA_PSS_SHA_512: function (base64: string): string {
throw new Error('Function not implemented.');
},
SM2DSA: function (base64: string): string {
throw new Error('Function not implemented.');
}
}
@Injectable()
export class SignHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsSign;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
Message: Joi.string().required(),
MessageType: Joi.string().required(),
SigningAlgorithm: Joi.string().required(),
});
protected async handle({ KeyId, Message, SigningAlgorithm }: QueryParams, { awsProperties } : RequestContext) {
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
if (!keyRecord) {
throw new NotFoundException();
}
if (!(keyRecord.metadata as any).SigningAlgorithms.includes(SigningAlgorithm)) {
throw new UnsupportedOperationException('Invalid signing algorithm');
}
const signature = signingAlgorithmToSigningFn[SigningAlgorithm as SigningAlgorithmSpec](Message, keyRecord);
return {
KeyId: keyRecord.arn,
Signature: signature,
SigningAlgorithm,
}
}
}

View File

@ -23,6 +23,7 @@ Date.prototype.getAwsTime = function (this: Date) {
const app = await NestFactory.create(AppModule);
// app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.useGlobalFilters(new AwsExceptionFilter());
app.use(bodyParser.json({ type: 'application/x-amz-json-1.0'}));
app.use(bodyParser.json({ type: 'application/x-amz-json-1.1'}));
const configService: ConfigService<CommonConfig, true> = app.get(ConfigService);

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
Name: string;
@ -28,11 +29,11 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
validator = Joi.object<QueryParams, true>({
Name: Joi.string().required(),
Description: Joi.string().allow('', null),
SecretString: Joi.string().allow('', null),
SecretString: Joi.string().allow('', null).default(''),
ClientRequestToken: Joi.string(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams, context: RequestContext) {
const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params;
@ -41,8 +42,8 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
description,
name,
secretString,
accountId: awsProperties.accountId,
region: awsProperties.region,
accountId: context.awsProperties.accountId,
region: context.awsProperties.region,
});
const arn = ArnUtil.fromSecret(secret);

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
SecretId: string;
@ -29,7 +30,7 @@ export class DeleteSecretHandler extends AbstractActionHandler {
VersionId: Joi.string().allow(null, ''),
});
protected async handle({ SecretId, VersionId }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ SecretId, VersionId }: QueryParams, { awsProperties} : RequestContext) {
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = VersionId ?

View File

@ -1,4 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
@ -6,6 +6,8 @@ import { Action } from '../action.enum';
import { TagsService } from '../aws-shared-entities/tags.service';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
SecretId: string;
@ -25,13 +27,13 @@ export class DescribeSecretHandler extends AbstractActionHandler {
action = Action.SecretsManagerDescribeSecret;
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ SecretId }: QueryParams, { awsProperties} : RequestContext) {
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
if (!secret) {
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
throw new NotFoundException();
}
const arn = ArnUtil.fromSecret(secret);

View File

@ -6,6 +6,7 @@ import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
SecretId: string;
@ -25,7 +26,7 @@ export class GetResourcePolicyHandler extends AbstractActionHandler {
action = Action.SecretsManagerGetResourcePolicy;
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ SecretId }: QueryParams, { awsProperties} : RequestContext) {
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);

View File

@ -5,6 +5,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
SecretId: string;
@ -27,7 +28,7 @@ export class GetSecretValueHandler extends AbstractActionHandler {
VersionId: Joi.string().allow(null, ''),
});
protected async handle({ SecretId, VersionId}: QueryParams, awsProperties: AwsProperties) {
protected async handle({ SecretId, VersionId}: QueryParams, { awsProperties} : RequestContext) {
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = VersionId ?

View File

@ -6,6 +6,7 @@ import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
SecretId: string;
@ -29,10 +30,10 @@ export class PutResourcePolicyHandler extends AbstractActionHandler {
ResourcePolicy: Joi.string().required(),
});
protected async handle({ SecretId, ResourcePolicy }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ SecretId, ResourcePolicy }: QueryParams, context: RequestContext) {
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
const secret = await this.secretService.findLatestByNameAndRegion(name, context.awsProperties.region);
if (!secret) {
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
ClientRequestToken?: string;
@ -30,11 +31,11 @@ export class PutSecretValueHandler extends AbstractActionHandler<QueryParams> {
SecretString: Joi.string(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams, context: RequestContext) {
const { SecretId, SecretString: secretString, ClientRequestToken } = params;
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const oldSecret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
const oldSecret = await this.secretService.findLatestByNameAndRegion(name, context.awsProperties.region);
if (!oldSecret) {
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
@ -44,8 +45,8 @@ export class PutSecretValueHandler extends AbstractActionHandler<QueryParams> {
versionId: ClientRequestToken ?? randomUUID(),
name: oldSecret.name,
secretString,
accountId: awsProperties.accountId,
region: awsProperties.region,
accountId: context.awsProperties.accountId,
region: context.awsProperties.region,
});
const arn = ArnUtil.fromSecret(secret);

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { TagsService } from '../aws-shared-entities/tags.service';
import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
Name: string;
@ -25,15 +26,15 @@ export class CreateTopicHandler extends AbstractActionHandler<QueryParams> {
action = Action.SnsCreateTopic;
validator = Joi.object<QueryParams, true>({ Name: Joi.string().required() });
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams, context: RequestContext) {
const { Name: name } = params;
const topic = await this.prismaService.snsTopic.create({
data: {
name,
accountId: awsProperties.accountId,
region: awsProperties.region,
accountId: context.awsProperties.accountId,
region: context.awsProperties.region,
},
});

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
SubscriptionArn: string;
@ -25,7 +26,7 @@ export class GetSubscriptionAttributesHandler extends AbstractActionHandler {
action = Action.SnsGetSubscriptionAttributes;
validator = Joi.object<QueryParams, true>({ SubscriptionArn: Joi.string().required() });
protected async handle({ SubscriptionArn }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ SubscriptionArn }: QueryParams, { awsProperties} : RequestContext) {
const id = SubscriptionArn.split(':').pop();
const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id }});

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
TopicArn: string;
@ -25,7 +26,7 @@ export class GetTopicAttributesHandler extends AbstractActionHandler {
action = Action.SnsGetTopicAttributes;
validator = Joi.object<QueryParams, true>({ TopicArn: Joi.string().required() });
protected async handle({ TopicArn }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ TopicArn }: QueryParams, { awsProperties} : RequestContext) {
const name = TopicArn.split(':').pop();
const topic = await this.prismaService.snsTopic.findFirst({ where: { name }});

View File

@ -4,6 +4,7 @@ import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { TagsService } from '../aws-shared-entities/tags.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
ResourceArn: string;
@ -22,7 +23,7 @@ export class ListTagsForResourceHandler extends AbstractActionHandler {
action = Action.SnsListTagsForResource;
validator = Joi.object<QueryParams, true>({ ResourceArn: Joi.string().required() });
protected async handle({ ResourceArn }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ ResourceArn }: QueryParams, { awsProperties} : RequestContext) {
const tags = await this.tagsService.getByArn(ResourceArn);
return TagsService.getXmlSafeTagsMap(tags);
}

View File

@ -5,6 +5,7 @@ import { PrismaService } from '../_prisma/prisma.service';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
NextToken: number;
@ -23,7 +24,7 @@ export class ListTopicsHandler extends AbstractActionHandler {
action = Action.SnsListTopics;
validator = Joi.object<QueryParams, true>({ NextToken: Joi.number().default(0) });
protected async handle({ NextToken: skip }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ NextToken: skip }: QueryParams, { awsProperties} : RequestContext) {
const [ topics, total ] = await Promise.all([
this.prismaService.snsTopic.findMany({ orderBy: { name: 'desc' }, take: 100, skip }),

View File

@ -9,6 +9,7 @@ import { AttributesService } from '../aws-shared-entities/attributes.service';
import { SqsQueueEntryService } from '../sqs/sqs-queue-entry.service';
import { SqsQueue } from '../sqs/sqs-queue.entity';
import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
TopicArn: string;
@ -37,7 +38,7 @@ export class PublishHandler extends AbstractActionHandler<QueryParams> {
Message: Joi.string().required(),
});
protected async handle({ TopicArn, TargetArn, Message, Subject }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ TopicArn, TargetArn, Message, Subject }: QueryParams, context: RequestContext) {
const arn = TopicArn ?? TargetArn;
if (!arn) {
@ -65,7 +66,7 @@ export class PublishHandler extends AbstractActionHandler<QueryParams> {
SignatureVersion: topicAttributes.find(a => a.name === 'SignatureVersion')?.value ?? '1',
Signature: '',
SigningCertURL: '',
UnsubscribeURL: `${awsProperties.host}/?Action=Unsubscribe&SubscriptionArn=${subArn}`,
UnsubscribeURL: `${context.awsProperties.host}/?Action=Unsubscribe&SubscriptionArn=${subArn}`,
});
await this.sqsQueueEntryService.publish(queueAccountId, queueName, message);

View File

@ -4,6 +4,7 @@ import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
AttributeName: string;
@ -28,7 +29,7 @@ export class SetSubscriptionAttributesHandler extends AbstractActionHandler<Quer
TopicArn: Joi.string().required(),
});
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, { awsProperties} : RequestContext) {
await this.attributeService.create({ name: AttributeName, value: AttributeValue, arn: TopicArn });
}
}

View File

@ -4,6 +4,7 @@ import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
AttributeName: string;
@ -28,7 +29,7 @@ export class SetTopicAttributesHandler extends AbstractActionHandler<QueryParams
TopicArn: Joi.string().required(),
});
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ AttributeName, AttributeValue, TopicArn }: QueryParams, { awsProperties} : RequestContext) {
await this.attributeService.create({ name: AttributeName, value: AttributeValue, arn: TopicArn });
}
}

View File

@ -3,11 +3,12 @@ import { randomUUID } from 'crypto';
import * as Joi from 'joi';
import { PrismaService } from '../_prisma/prisma.service';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { AbstractActionHandler, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { TagsService } from '../aws-shared-entities/tags.service';
import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
TopicArn: string;
@ -34,7 +35,7 @@ export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
Protocol: Joi.string().required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams, context: RequestContext) {
const subscription = await this.prismaService.snsTopicSubscription.create({
data: {
@ -42,8 +43,8 @@ export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
topicArn: params.TopicArn,
protocol: params.Protocol,
endpoint: params.Endpoint,
accountId: awsProperties.accountId,
region: awsProperties.region,
accountId: context.awsProperties.accountId,
region: context.awsProperties.region,
}
});

View File

@ -29,7 +29,7 @@ export class UnsubscribeHandler extends AbstractActionHandler<QueryParams> {
SubscriptionArn: Joi.string().required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams) {
const id = params.SubscriptionArn.split(':').pop();
const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id } });

View File

@ -1,48 +1,18 @@
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { TagsService } from '../aws-shared-entities/tags.service';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity';
import { V2CreateQueueHandler } from './v2-create-queue.handler';
type QueryParams = {
QueueName: string;
}
@Injectable()
export class CreateQueueHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly sqsQueueEntryService: SqsQueueEntryService,
private readonly tagsService: TagsService,
private readonly attributeService: AttributesService,
) {
super();
}
export class CreateQueueHandler extends V2CreateQueueHandler {
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.sqsQueueEntryService.createQueue({
name,
accountId: awsProperties.accountId,
region: awsProperties.region,
});
const tags = TagsService.tagPairs(params);
await this.tagsService.createMany(queue.arn, tags);
const attributes = SqsQueue.attributePairs(params);
await this.attributeService.createMany(queue.arn, attributes);
return { QueueUrl: queue.getUrl(awsProperties.host) };
}
}

View File

@ -5,6 +5,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
QueueUrl: string;
@ -26,7 +27,7 @@ export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams
QueueUrl: Joi.string().required(),
});
protected async handle( params : QueryParams, awsProperties: AwsProperties) {
protected async handle( params : QueryParams, { awsProperties} : RequestContext) {
const { QueueUrl } = params;
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);

View File

@ -5,6 +5,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
QueueUrl: string;
@ -28,7 +29,7 @@ export class DeleteMessageHandler extends AbstractActionHandler<QueryParams> {
ReceiptHandle: Joi.string().required(),
});
protected async handle({ QueueUrl, ReceiptHandle }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ QueueUrl, ReceiptHandle }: QueryParams, { awsProperties} : RequestContext) {
await this.sqsQueueEntryService.deleteMessage(ReceiptHandle);
}
}

View File

@ -31,7 +31,7 @@ export class DeleteQueueHandler extends AbstractActionHandler<QueryParams> {
__path: Joi.string().required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams) {
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(params.QueueUrl ?? params.__path);
const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name);

View File

@ -31,7 +31,7 @@ export class GetQueueAttributesHandler extends AbstractActionHandler<QueryParams
__path: Joi.string().required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams) {
const attributeNames = Object.keys(params).reduce((l, k) => {
const [name, _] = k.split('.');

View File

@ -1,39 +1,18 @@
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { PrismaService } from '../_prisma/prisma.service';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { SqsQueue } from './sqs-queue.entity';
import { V2ListQueuesHandler } from './v2-list-queues.handler';
import { RequestContext } from '../_context/request.context';
type QueryParams = {}
@Injectable()
export class ListQueuesHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly prismaService: PrismaService,
) {
super();
}
export class ListQueuesHandler extends V2ListQueuesHandler {
format = Format.Xml;
action = Action.SqsListQueues;
validator = Joi.object<QueryParams, true>();
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
const rawQueues = await this.prismaService.sqsQueue.findMany({
where: {
accountId: awsProperties.accountId,
region: awsProperties.region,
}
});
const queues = rawQueues.map(q => new SqsQueue(q));
return {
QueueUrl: queues.map((q) => q.getUrl(awsProperties.host))
}
override async handle(params: {}, context: RequestContext) {
const response: any = await super.handle(params, context);
return { QueueUrl: response.QueueUrls }
}
}

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi';
import { SqsQueue } from './sqs-queue.entity';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
QueueUrl: string;
@ -22,7 +23,7 @@ export class PurgeQueueHandler extends AbstractActionHandler<QueryParams> {
action = Action.SqsPurgeQueue;
validator = Joi.object<QueryParams, true>({ QueueUrl: Joi.string().required() });
protected async handle({ QueueUrl }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ QueueUrl }: QueryParams, { awsProperties} : RequestContext) {
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
await this.sqsQueueEntryService.purge(accountId, name);

View File

@ -30,7 +30,7 @@ export class ReceiveMessageHandler extends AbstractActionHandler<QueryParams> {
VisibilityTimeout: Joi.number(),
});
protected async handle({ QueueUrl, MaxNumberOfMessages, VisibilityTimeout }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ QueueUrl, MaxNumberOfMessages, VisibilityTimeout }: QueryParams) {
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
const records = await this.sqsQueueEntryService.receiveMessages(accountId, name, MaxNumberOfMessages, VisibilityTimeout);

View File

@ -31,7 +31,7 @@ export class SetQueueAttributesHandler extends AbstractActionHandler<QueryParams
__path: Joi.string().required(),
});
protected async handle(params: QueryParams, awsProperties: AwsProperties) {
protected async handle(params: QueryParams) {
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(params.__path);
const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name);
const attributes = SqsQueue.attributePairs(params);

View File

@ -4,6 +4,7 @@ import { randomUUID } from 'crypto';
import { PrismaService } from '../_prisma/prisma.service';
import { SqsQueue } from './sqs-queue.entity';
import { QueueNameExists } from '../aws-shared-entities/aws-exceptions';
type QueueEntry = {
id: string;
@ -33,8 +34,12 @@ export class SqsQueueEntryService {
}
async createQueue(data: Prisma.SqsQueueCreateInput): Promise<SqsQueue> {
const prisma = await this.prismaService.sqsQueue.create({ data });
return new SqsQueue(prisma);
try {
const prisma = await this.prismaService.sqsQueue.create({ data });
return new SqsQueue(prisma);
} catch (error) {
throw new QueueNameExists();
}
}
async deleteQueue(id: number): Promise<void> {

View File

@ -54,7 +54,12 @@ const attributeSlotMap = {
return SqsQueue.getAccountIdAndNameFromPath(workingString);
}
static attributePairs(queryParams: Record<string, string>): { key: string, value: string }[] {
static attributePairs(queryParams: Record<string, any>): { key: string, value: string }[] {
if (queryParams.Attributes) {
return Object.entries(queryParams.Attributes as Record<string, string>).map(([key, value]) => ({ key, value }));
}
const pairs: { key: string, value: string }[] = [];
for (const param of Object.keys(queryParams)) {
const components = this.breakdownAwsQueryParam(param);

View File

@ -17,6 +17,8 @@ import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsHandlers } from './sqs.constants';
import { DeleteMessageBatchHandler } from './delete-message-batch.handler';
import { PrismaModule } from '../_prisma/prisma.module';
import { V2ListQueuesHandler } from './v2-list-queues.handler';
import { V2CreateQueueHandler } from './v2-create-queue.handler';
const handlers = [
CreateQueueHandler,
@ -28,6 +30,8 @@ const handlers = [
PurgeQueueHandler,
ReceiveMessageHandler,
SetQueueAttributesHandler,
V2CreateQueueHandler,
V2ListQueuesHandler,
]
const actions = [
@ -51,6 +55,26 @@ const actions = [
Action.SqsSetQueueAttributes,
Action.SqsTagQueue,
Action.SqsUntagQueue,
Action.V2_SqsAddPermisson,
Action.V2_SqsChangeMessageVisibility,
Action.V2_SqsChangeMessageVisibilityBatch,
Action.V2_SqsCreateQueue,
Action.V2_SqsDeleteMessage,
Action.V2_SqsDeleteMessageBatch,
Action.V2_SqsDeleteQueue,
Action.V2_SqsGetQueueAttributes,
Action.V2_SqsGetQueueUrl,
Action.V2_SqsListDeadLetterSourceQueues,
Action.V2_SqsListQueues,
Action.V2_SqsListQueueTags,
Action.V2_SqsPurgeQueue,
Action.V2_SqsReceiveMessage,
Action.V2_SqsRemovePermission,
Action.V2_SqsSendMessage,
Action.V2_SqsSendMessageBatch,
Action.V2_SqsSetQueueAttributes,
Action.V2_SqsTagQueue,
Action.V2_SqsUntagQueue,
]
@Module({

View File

@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service';
import { TagsService } from '../aws-shared-entities/tags.service';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity';
import { RequestContext } from '../_context/request.context';
type QueryParams = {
QueueName: string;
}
@Injectable()
export class V2CreateQueueHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly sqsQueueEntryService: SqsQueueEntryService,
private readonly tagsService: TagsService,
private readonly attributeService: AttributesService,
) {
super();
}
format = Format.Json;
action = Action.V2_SqsCreateQueue;
validator = Joi.object<QueryParams, true>({
QueueName: Joi.string().required(),
});
protected async handle(params: QueryParams, context: RequestContext) {
const { QueueName: name } = params;
const queue = await this.sqsQueueEntryService.createQueue({
name,
accountId: context.awsProperties.accountId,
region: context.awsProperties.region,
});
const tags = TagsService.tagPairs(params);
await this.tagsService.createMany(queue.arn, tags);
const attributes = SqsQueue.attributePairs(params);
await this.attributeService.createMany(queue.arn, attributes);
return { QueueUrl: queue.getUrl(context.awsProperties.host) };
}
}

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { PrismaService } from '../_prisma/prisma.service';
import { AbstractActionHandler, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { SqsQueue } from './sqs-queue.entity';
import { RequestContext } from '../_context/request.context';
type QueryParams = {}
@Injectable()
export class V2ListQueuesHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly prismaService: PrismaService,
) {
super();
}
format = Format.Json;
action = Action.V2_SqsListQueues;
validator = Joi.object<QueryParams, true>();
protected async handle(params: QueryParams, context: RequestContext): Promise<{ QueueUrl: string[] } | { QueueUrls: string[] } > {
const rawQueues = await this.prismaService.sqsQueue.findMany({
where: {
accountId: context.awsProperties.accountId,
region: context.awsProperties.region,
}
});
const queues = rawQueues.map(q => new SqsQueue(q));
return {
QueueUrls: queues.map((q) => q.getUrl(context.awsProperties.host))
}
}
}

View File

@ -3,6 +3,7 @@ import * as Joi from "joi";
import { AbstractActionHandler, AwsProperties, Format } from "../abstract-action.handler";
import { Action } from "../action.enum";
import { RequestContext } from "../_context/request.context";
type QueryParams = {}
@ -13,11 +14,11 @@ export class GetCallerIdentityHandler extends AbstractActionHandler<QueryParams>
action = Action.StsGetCallerIdentity;
validator = Joi.object<QueryParams, true>();
protected async handle(queryParams: QueryParams, awsProperties: AwsProperties) {
protected async handle(queryParams: QueryParams, context: RequestContext) {
return {
"UserId": "AIDASAMPLEUSERID",
"Account": awsProperties.accountId,
"Arn": `arn:aws:iam::${awsProperties.accountId}:user/DevAdmin`
"Account": context.awsProperties.accountId,
"Arn": `arn:aws:iam::${context.awsProperties.accountId}:user/DevAdmin`
}
}
}

View File

@ -0,0 +1,53 @@
class LinkedListNode<T> {
previous: LinkedListNode<T> | null;
next: LinkedListNode<T> | null;
constructor(
readonly record: T,
) {
this.previous = null;
this.next = null;
}
}
export class Stack<T> {
private head: LinkedListNode<T> | null = null;
private tail: LinkedListNode<T> | null = null;
private size = 0;
constructor(
private readonly maxSize: number,
) {}
/*
Add E
D ... B <-> A
Cull A
*/
push(record: T): Stack<T> {
const D = this.head;
const E = new LinkedListNode(record);
this.head = E;
if (D === null) {
this.tail = E;
this.size++;
return this;
}
if (this.size < this.maxSize) {
E.next = D;
D.previous = E;
this.size++;
return this;
}
const A = this.tail;
const B = A!.previous!;
B.next = null;
this.tail = B;
return this;
}
}