diff --git a/README.md b/README.md index 1dbeee5..53a4422 100644 --- a/README.md +++ b/README.md @@ -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 | void +* handle(queryParams: T, { awsProperties} : RequestContext): Record | void * the method that implements the AWS action diff --git a/prisma/migrations/20241220030043_/migration.sql b/prisma/migrations/20241220030043_/migration.sql deleted file mode 100644 index a415abe..0000000 --- a/prisma/migrations/20241220030043_/migration.sql +++ /dev/null @@ -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"); diff --git a/prisma/migrations/20241220053141_/migration.sql b/prisma/migrations/20241220053141_/migration.sql deleted file mode 100644 index af417e4..0000000 --- a/prisma/migrations/20241220053141_/migration.sql +++ /dev/null @@ -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 -); diff --git a/prisma/migrations/20241220224222_/migration.sql b/prisma/migrations/20241220224222_/migration.sql deleted file mode 100644 index e8fc604..0000000 --- a/prisma/migrations/20241220224222_/migration.sql +++ /dev/null @@ -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; diff --git a/prisma/migrations/20241221004838_/migration.sql b/prisma/migrations/20241221004838_/migration.sql deleted file mode 100644 index 4e55ad7..0000000 --- a/prisma/migrations/20241221004838_/migration.sql +++ /dev/null @@ -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; diff --git a/prisma/migrations/20241221010230_/migration.sql b/prisma/migrations/20241221010230_/migration.sql deleted file mode 100644 index f6ea731..0000000 --- a/prisma/migrations/20241221010230_/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- AlterTable -ALTER TABLE "KmsKey" ADD COLUMN "nextRotation" DATETIME; -ALTER TABLE "KmsKey" ADD COLUMN "rotationPeriod" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0d57aa4..cca2328 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/src/_context/request.context.ts b/src/_context/request.context.ts index ce83c40..2d948b1 100644 --- a/src/_context/request.context.ts +++ b/src/_context/request.context.ts @@ -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; } diff --git a/src/abstract-action.handler.ts b/src/abstract-action.handler.ts index d2ed356..365ad39 100644 --- a/src/abstract-action.handler.ts +++ b/src/abstract-action.handler.ts @@ -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; - protected abstract handle(queryParams: T, awsProperties: AwsProperties): Record | void; + protected abstract handle(queryParams: T, context: RequestContext): Record | 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, @Headers() headers: Record, ) { @@ -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; } diff --git a/src/app.module.ts b/src/app.module.ts index 8586476..302de68 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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'; diff --git a/src/audit/audit.controller.ts b/src/audit/audit.controller.ts new file mode 100644 index 0000000..d4054b9 --- /dev/null +++ b/src/audit/audit.controller.ts @@ -0,0 +1,11 @@ +import { Controller } from "@nestjs/common"; +import { AuditService } from "./audit.service"; + +@Controller('_audit') +export class AuditController { + + constructor( + private readonly auditService: AuditService, + ) {} + +} diff --git a/src/_context/audit.interceptor.ts b/src/audit/audit.interceptor.ts similarity index 88% rename from src/_context/audit.interceptor.ts rename to src/audit/audit.interceptor.ts index 6b29d1c..16f35c9 100644 --- a/src/_context/audit.interceptor.ts +++ b/src/audit/audit.interceptor.ts @@ -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 implements NestInterceptor { @Inject(ActionHandlers) private readonly handlers: ActionHandlers, private readonly prismaService: PrismaService, + private readonly configService: ConfigService, ) {} intercept(context: ExecutionContext, next: CallHandler): Observable { + 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(); diff --git a/src/audit/audit.module.ts b/src/audit/audit.module.ts new file mode 100644 index 0000000..87581ad --- /dev/null +++ b/src/audit/audit.module.ts @@ -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 {} diff --git a/src/audit/audit.service.ts b/src/audit/audit.service.ts new file mode 100644 index 0000000..9ab2106 --- /dev/null +++ b/src/audit/audit.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from "@nestjs/common"; + +import { PrismaService } from "../_prisma/prisma.service"; + +@Injectable() +export class AuditService { + + + + constructor( + private readonly prismaService: PrismaService, + ) {} + +} diff --git a/src/aws-shared-entities/aws-exceptions.ts b/src/aws-shared-entities/aws-exceptions.ts index 32886d1..ac3cff4 100644 --- a/src/aws-shared-entities/aws-exceptions.ts +++ b/src/aws-shared-entities/aws-exceptions.ts @@ -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, + ) + } +} \ No newline at end of file diff --git a/src/aws-shared-entities/tags.service.ts b/src/aws-shared-entities/tags.service.ts index fd4c2e5..1092940 100644 --- a/src/aws-shared-entities/tags.service.ts +++ b/src/aws-shared-entities/tags.service.ts @@ -37,7 +37,7 @@ export class TagsService { await this.prismaService.tag.deleteMany({ where: { arn, name } }); } - static tagPairs(queryParams: Record): { key: string, value: string }[] { + static tagPairs(queryParams: Record): { key: string, value: string }[] { const pairs: { key: string, value: string }[] = []; for (const param of Object.keys(queryParams)) { const components = breakdownAwsQueryParam(param); diff --git a/src/default-action-handler/existing-action-handlers.provider.ts b/src/default-action-handler/existing-action-handlers.provider.ts index 0550234..97881ae 100644 --- a/src/default-action-handler/existing-action-handlers.provider.ts +++ b/src/default-action-handler/existing-action-handlers.provider.ts @@ -7,6 +7,14 @@ import { ExistingActionHandlers } from './default-action-handler.constants'; export const ExistingActionHandlersProvider = (inject: Array): 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), diff --git a/src/iam/attach-role-policy.handler.ts b/src/iam/attach-role-policy.handler.ts index 2809aa2..6376a46 100644 --- a/src/iam/attach-role-policy.handler.ts +++ b/src/iam/attach-role-policy.handler.ts @@ -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 { constructor( - + private readonly iamService: IamService, ) { super(); } @@ -24,8 +26,12 @@ export class AttachRolePolicyHandler extends AbstractActionHandler 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 + ); } } diff --git a/src/iam/create-policy-version.handler.ts b/src/iam/create-policy-version.handler.ts index a649897..976bfac 100644 --- a/src/iam/create-policy-version.handler.ts +++ b/src/iam/create-policy-version.handler.ts @@ -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 { 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 { path: Path, description: Description, policy: PolicyDocument, - accountId: awsProperties.accountId, + accountId: context.awsProperties.accountId, }); - return policy.metadata; + return { + Policy: policy.metadata + }; } } diff --git a/src/iam/create-role.handler.ts b/src/iam/create-role.handler.ts index 294baeb..93d7b26 100644 --- a/src/iam/create-role.handler.ts +++ b/src/iam/create-role.handler.ts @@ -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 { 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(), diff --git a/src/iam/delete-role.handler.ts b/src/iam/delete-role.handler.ts index e1c6f86..fd721c2 100644 --- a/src/iam/delete-role.handler.ts +++ b/src/iam/delete-role.handler.ts @@ -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 { 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); } } diff --git a/src/iam/get-policy-version.handler.ts b/src/iam/get-policy-version.handler.ts index d8f473c..0ac1116 100644 --- a/src/iam/get-policy-version.handler.ts +++ b/src/iam/get-policy-version.handler.ts @@ -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 { constructor( - + private readonly iamService: IamService, ) { super(); } @@ -24,7 +26,17 @@ export class GetPolicyVersionHandler extends AbstractActionHandler 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(), + } + } } } diff --git a/src/iam/get-policy.handler.ts b/src/iam/get-policy.handler.ts index bdc7fb1..2248b9b 100644 --- a/src/iam/get-policy.handler.ts +++ b/src/iam/get-policy.handler.ts @@ -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 { constructor( - + private readonly iamService: IamService, ) { super(); } @@ -22,7 +24,10 @@ export class GetPolicyHandler extends AbstractActionHandler { 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, + } } } diff --git a/src/iam/get-role.handler.ts b/src/iam/get-role.handler.ts index cac7228..8890568 100644 --- a/src/iam/get-role.handler.ts +++ b/src/iam/get-role.handler.ts @@ -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 { 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, } diff --git a/src/iam/iam.module.ts b/src/iam/iam.module.ts index 5db95f8..d958f98 100644 --- a/src/iam/iam.module.ts +++ b/src/iam/iam.module.ts @@ -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 = [ diff --git a/src/iam/iam.service.ts b/src/iam/iam.service.ts index 7c4c865..99cca75 100644 --- a/src/iam/iam.service.ts +++ b/src/iam/iam.service.ts @@ -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 { - const record = await this.prismaService.iamRole.findFirst({ - where: { - name, - accountId, - } - }); - - return record ? new IamRole(record) : null; + async findOneRoleByName(accountId: string, name: string): Promise { + 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 { - const record = await this.prismaService.iamPolicy.create({ data }); - return new IamPolicy(record); + async listRolePolicies(): Promise { + // return await this.prismaService; + return []; } + + async getPolicyByArn(arn: string): Promise { + 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 { + 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 { + 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 { + 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(); + } + } } diff --git a/src/iam/list-attached-role-policies.ts b/src/iam/list-attached-role-policies.ts index 84c3406..b35b11e 100644 --- a/src/iam/list-attached-role-policies.ts +++ b/src/iam/list-attached-role-policies.ts @@ -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 { constructor( + private readonly iamService: IamService, ) { super(); } @@ -21,8 +24,15 @@ export class ListAttachedRolePoliciesHandler extends AbstractActionHandler ({ + member: { + PolicyName: p.name, + PolicyArn: p.arn, + } + })), + } } } diff --git a/src/iam/list-role-policies.handler.ts b/src/iam/list-role-policies.handler.ts index 173c884..6f6ba72 100644 --- a/src/iam/list-role-policies.handler.ts +++ b/src/iam/list-role-policies.handler.ts @@ -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 RoleName: Joi.string().required(), }); - protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) { + protected async handle({ RoleName }: QueryParams, { awsProperties} : RequestContext) { + } } diff --git a/src/kms/create-alias.handler.ts b/src/kms/create-alias.handler.ts index 8c506da..09f05fc 100644 --- a/src/kms/create-alias.handler.ts +++ b/src/kms/create-alias.handler.ts @@ -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 { 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); diff --git a/src/kms/create-key.handler.ts b/src/kms/create-key.handler.ts index 71a2d28..681abb4 100644 --- a/src/kms/create-key.handler.ts +++ b/src/kms/create-key.handler.ts @@ -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 = { [P in keyof T]-?: NoUndefinedField> }; @@ -71,7 +72,7 @@ export class CreateKeyHandler extends AbstractActionHandler { }) 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; diff --git a/src/kms/describe-key.handler.ts b/src/kms/describe-key.handler.ts index b7d7cb6..8a33ba2 100644 --- a/src/kms/describe-key.handler.ts +++ b/src/kms/describe-key.handler.ts @@ -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 { 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); diff --git a/src/kms/enable-key-rotation.handler.ts b/src/kms/enable-key-rotation.handler.ts index a047eb8..de008d2 100644 --- a/src/kms/enable-key-rotation.handler.ts +++ b/src/kms/enable-key-rotation.handler.ts @@ -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 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(); diff --git a/src/kms/get-key-policy.handler.ts b/src/kms/get-key-policy.handler.ts index d782164..92ae6c5 100644 --- a/src/kms/get-key-policy.handler.ts +++ b/src/kms/get-key-policy.handler.ts @@ -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 { 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(); diff --git a/src/kms/get-key-rotation-status.handler.ts b/src/kms/get-key-rotation-status.handler.ts index f24472d..0ca558b 100644 --- a/src/kms/get-key-rotation-status.handler.ts +++ b/src/kms/get-key-rotation-status.handler.ts @@ -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 { + + constructor( + private readonly kmsService: KmsService, + ) { + super(); + } + + format = Format.Json; + action = Action.KmsGetPublicKey; + validator = Joi.object({ + 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'), + } + } +} diff --git a/src/kms/kms-key.entity.ts b/src/kms/kms-key.entity.ts index a018fcf..080b8d3 100644 --- a/src/kms/kms-key.entity.ts +++ b/src/kms/kms-key.entity.ts @@ -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() { diff --git a/src/kms/kms.module.ts b/src/kms/kms.module.ts index 8e82875..6105bba 100644 --- a/src/kms/kms.module.ts +++ b/src/kms/kms.module.ts @@ -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 = [ diff --git a/src/kms/kms.service.ts b/src/kms/kms.service.ts index 38c442d..ce9ea41 100644 --- a/src/kms/kms.service.ts +++ b/src/kms/kms.service.ts @@ -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 { diff --git a/src/kms/list-aliases.handler.ts b/src/kms/list-aliases.handler.ts index 3ef5257..e3a7cf9 100644 --- a/src/kms/list-aliases.handler.ts +++ b/src/kms/list-aliases.handler.ts @@ -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 { 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) diff --git a/src/kms/list-resource-tags.handler.ts b/src/kms/list-resource-tags.handler.ts index de36c20..dccb491 100644 --- a/src/kms/list-resource-tags.handler.ts +++ b/src/kms/list-resource-tags.handler.ts @@ -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 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(); diff --git a/src/kms/sign.handler.ts b/src/kms/sign.handler.ts new file mode 100644 index 0000000..61b73fe --- /dev/null +++ b/src/kms/sign.handler.ts @@ -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 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 { + + constructor( + private readonly kmsService: KmsService, + ) { + super(); + } + + format = Format.Json; + action = Action.KmsSign; + validator = Joi.object({ + 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, + } + } +} diff --git a/src/main.ts b/src/main.ts index 3acdbe4..3f77366 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 = app.get(ConfigService); diff --git a/src/secrets-manager/create-secret.handler.ts b/src/secrets-manager/create-secret.handler.ts index d7d7a58..05aa35b 100644 --- a/src/secrets-manager/create-secret.handler.ts +++ b/src/secrets-manager/create-secret.handler.ts @@ -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 { validator = Joi.object({ 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 { description, name, secretString, - accountId: awsProperties.accountId, - region: awsProperties.region, + accountId: context.awsProperties.accountId, + region: context.awsProperties.region, }); const arn = ArnUtil.fromSecret(secret); diff --git a/src/secrets-manager/delete-secret.handler.ts b/src/secrets-manager/delete-secret.handler.ts index 347fbcd..04f08bc 100644 --- a/src/secrets-manager/delete-secret.handler.ts +++ b/src/secrets-manager/delete-secret.handler.ts @@ -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 ? diff --git a/src/secrets-manager/describe-secret.handler.ts b/src/secrets-manager/describe-secret.handler.ts index d95c108..2d4a60c 100644 --- a/src/secrets-manager/describe-secret.handler.ts +++ b/src/secrets-manager/describe-secret.handler.ts @@ -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({ 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); diff --git a/src/secrets-manager/get-resource-policy.handler.ts b/src/secrets-manager/get-resource-policy.handler.ts index 8938d2c..f4525a3 100644 --- a/src/secrets-manager/get-resource-policy.handler.ts +++ b/src/secrets-manager/get-resource-policy.handler.ts @@ -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({ 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); diff --git a/src/secrets-manager/get-secret-value.handler.ts b/src/secrets-manager/get-secret-value.handler.ts index ef6cd4d..bec270c 100644 --- a/src/secrets-manager/get-secret-value.handler.ts +++ b/src/secrets-manager/get-secret-value.handler.ts @@ -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 ? diff --git a/src/secrets-manager/put-resource-policy.handler.ts b/src/secrets-manager/put-resource-policy.handler.ts index 0761c55..001c528 100644 --- a/src/secrets-manager/put-resource-policy.handler.ts +++ b/src/secrets-manager/put-resource-policy.handler.ts @@ -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."); diff --git a/src/secrets-manager/put-secret-value.handler.ts b/src/secrets-manager/put-secret-value.handler.ts index 329f1ea..6365ae6 100644 --- a/src/secrets-manager/put-secret-value.handler.ts +++ b/src/secrets-manager/put-secret-value.handler.ts @@ -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 { 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 { 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); diff --git a/src/sns/create-topic.handler.ts b/src/sns/create-topic.handler.ts index 6a92f42..3a8e179 100644 --- a/src/sns/create-topic.handler.ts +++ b/src/sns/create-topic.handler.ts @@ -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 { action = Action.SnsCreateTopic; validator = Joi.object({ 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, }, }); diff --git a/src/sns/get-subscription-attributes.handler.ts b/src/sns/get-subscription-attributes.handler.ts index 0182f6b..0bf59f6 100644 --- a/src/sns/get-subscription-attributes.handler.ts +++ b/src/sns/get-subscription-attributes.handler.ts @@ -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({ 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 }}); diff --git a/src/sns/get-topic-attributes.handler.ts b/src/sns/get-topic-attributes.handler.ts index 1c0aebf..42f3cde 100644 --- a/src/sns/get-topic-attributes.handler.ts +++ b/src/sns/get-topic-attributes.handler.ts @@ -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({ 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 }}); diff --git a/src/sns/list-tags-for-resource.handler.ts b/src/sns/list-tags-for-resource.handler.ts index c1e928c..6386096 100644 --- a/src/sns/list-tags-for-resource.handler.ts +++ b/src/sns/list-tags-for-resource.handler.ts @@ -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({ 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); } diff --git a/src/sns/list-topics.handler.ts b/src/sns/list-topics.handler.ts index c2489a6..3d74b3e 100644 --- a/src/sns/list-topics.handler.ts +++ b/src/sns/list-topics.handler.ts @@ -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({ 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 }), diff --git a/src/sns/publish.handler.ts b/src/sns/publish.handler.ts index b1d3040..7009745 100644 --- a/src/sns/publish.handler.ts +++ b/src/sns/publish.handler.ts @@ -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 { 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 { 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); diff --git a/src/sns/set-subscription-attributes.handler.ts b/src/sns/set-subscription-attributes.handler.ts index b20a4f4..de2deae 100644 --- a/src/sns/set-subscription-attributes.handler.ts +++ b/src/sns/set-subscription-attributes.handler.ts @@ -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 { 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 { topicArn: params.TopicArn, protocol: params.Protocol, endpoint: params.Endpoint, - accountId: awsProperties.accountId, - region: awsProperties.region, + accountId: context.awsProperties.accountId, + region: context.awsProperties.region, } }); diff --git a/src/sns/unsubscribe.handler.ts b/src/sns/unsubscribe.handler.ts index 56d8879..ea2438c 100644 --- a/src/sns/unsubscribe.handler.ts +++ b/src/sns/unsubscribe.handler.ts @@ -29,7 +29,7 @@ export class UnsubscribeHandler extends AbstractActionHandler { 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 } }); diff --git a/src/sqs/create-queue.handler.ts b/src/sqs/create-queue.handler.ts index 059ef5c..e649bc5 100644 --- a/src/sqs/create-queue.handler.ts +++ b/src/sqs/create-queue.handler.ts @@ -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 { - - 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({ 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) }; - } } diff --git a/src/sqs/delete-message-batch.handler.ts b/src/sqs/delete-message-batch.handler.ts index d9f8b72..dccf291 100644 --- a/src/sqs/delete-message-batch.handler.ts +++ b/src/sqs/delete-message-batch.handler.ts @@ -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 { 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); } } diff --git a/src/sqs/delete-queue.handler.ts b/src/sqs/delete-queue.handler.ts index 31640a9..7d432d0 100644 --- a/src/sqs/delete-queue.handler.ts +++ b/src/sqs/delete-queue.handler.ts @@ -31,7 +31,7 @@ export class DeleteQueueHandler extends AbstractActionHandler { __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); diff --git a/src/sqs/get-queue-attributes.handler.ts b/src/sqs/get-queue-attributes.handler.ts index c91e95e..b9a33f3 100644 --- a/src/sqs/get-queue-attributes.handler.ts +++ b/src/sqs/get-queue-attributes.handler.ts @@ -31,7 +31,7 @@ export class GetQueueAttributesHandler extends AbstractActionHandler { const [name, _] = k.split('.'); diff --git a/src/sqs/list-queues.handler.ts b/src/sqs/list-queues.handler.ts index bc24230..575a2dc 100644 --- a/src/sqs/list-queues.handler.ts +++ b/src/sqs/list-queues.handler.ts @@ -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 { - - constructor( - private readonly prismaService: PrismaService, - ) { - super(); - } - +export class ListQueuesHandler extends V2ListQueuesHandler { format = Format.Xml; action = Action.SqsListQueues; - validator = Joi.object(); - 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 } } } diff --git a/src/sqs/purge-queue.handler.ts b/src/sqs/purge-queue.handler.ts index 8f24d44..fa97b62 100644 --- a/src/sqs/purge-queue.handler.ts +++ b/src/sqs/purge-queue.handler.ts @@ -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 { action = Action.SqsPurgeQueue; validator = Joi.object({ 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); diff --git a/src/sqs/receive-message.handler.ts b/src/sqs/receive-message.handler.ts index 4436e48..84a51a7 100644 --- a/src/sqs/receive-message.handler.ts +++ b/src/sqs/receive-message.handler.ts @@ -30,7 +30,7 @@ export class ReceiveMessageHandler extends AbstractActionHandler { 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); diff --git a/src/sqs/set-queue-attributes.handler.ts b/src/sqs/set-queue-attributes.handler.ts index 6e46482..f92bb62 100644 --- a/src/sqs/set-queue-attributes.handler.ts +++ b/src/sqs/set-queue-attributes.handler.ts @@ -31,7 +31,7 @@ export class SetQueueAttributesHandler extends AbstractActionHandler { - 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 { diff --git a/src/sqs/sqs-queue.entity.ts b/src/sqs/sqs-queue.entity.ts index aef0c99..9c2d112 100644 --- a/src/sqs/sqs-queue.entity.ts +++ b/src/sqs/sqs-queue.entity.ts @@ -54,7 +54,12 @@ const attributeSlotMap = { return SqsQueue.getAccountIdAndNameFromPath(workingString); } - static attributePairs(queryParams: Record): { key: string, value: string }[] { + static attributePairs(queryParams: Record): { key: string, value: string }[] { + + if (queryParams.Attributes) { + return Object.entries(queryParams.Attributes as Record).map(([key, value]) => ({ key, value })); + } + const pairs: { key: string, value: string }[] = []; for (const param of Object.keys(queryParams)) { const components = this.breakdownAwsQueryParam(param); diff --git a/src/sqs/sqs.module.ts b/src/sqs/sqs.module.ts index 8e8949c..e064952 100644 --- a/src/sqs/sqs.module.ts +++ b/src/sqs/sqs.module.ts @@ -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({ diff --git a/src/sqs/v2-create-queue.handler.ts b/src/sqs/v2-create-queue.handler.ts new file mode 100644 index 0000000..17915a5 --- /dev/null +++ b/src/sqs/v2-create-queue.handler.ts @@ -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 { + + constructor( + private readonly sqsQueueEntryService: SqsQueueEntryService, + private readonly tagsService: TagsService, + private readonly attributeService: AttributesService, + ) { + super(); + } + + format = Format.Json; + action = Action.V2_SqsCreateQueue; + validator = Joi.object({ + 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) }; + } +} diff --git a/src/sqs/v2-list-queues.handler.ts b/src/sqs/v2-list-queues.handler.ts new file mode 100644 index 0000000..3fee2eb --- /dev/null +++ b/src/sqs/v2-list-queues.handler.ts @@ -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 { + + constructor( + private readonly prismaService: PrismaService, + ) { + super(); + } + + format = Format.Json; + action = Action.V2_SqsListQueues; + validator = Joi.object(); + + 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)) + } + } +} diff --git a/src/sts/get-caller-identity.handler.ts b/src/sts/get-caller-identity.handler.ts index 7e431b0..7e78e58 100644 --- a/src/sts/get-caller-identity.handler.ts +++ b/src/sts/get-caller-identity.handler.ts @@ -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 action = Action.StsGetCallerIdentity; validator = Joi.object(); - 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` } } } diff --git a/src/util/stack.datatype.ts b/src/util/stack.datatype.ts new file mode 100644 index 0000000..9684307 --- /dev/null +++ b/src/util/stack.datatype.ts @@ -0,0 +1,53 @@ +class LinkedListNode { + previous: LinkedListNode | null; + next: LinkedListNode | null; + + constructor( + readonly record: T, + ) { + this.previous = null; + this.next = null; + } +} + +export class Stack { + + private head: LinkedListNode | null = null; + private tail: LinkedListNode | null = null; + private size = 0; + + constructor( + private readonly maxSize: number, + ) {} + + /* + Add E + D ... B <-> A + Cull A + */ + push(record: T): Stack { + + 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; + } +}