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) * format: the format for output (XML or JSON)
* action: the action the handler is implementing (will be use to key by) * 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 * 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 * 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 accountId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
policies IamRoleIamPolicyAttachment[]
@@unique([accountId, name]) @@unique([accountId, name])
} }
model IamPolicy { model IamPolicy {
id String id String
version Int @default(1) version Int @default(1)
isDefault Boolean isDefault Boolean
path String? path String?
name String name String
@ -57,6 +59,15 @@ model IamPolicy {
@@unique([accountId, path, name]) @@unique([accountId, path, name])
} }
model IamRoleIamPolicyAttachment {
iamRoleId String
iamPolicyId String
role IamRole @relation(fields: [iamRoleId], references: [id])
@@id([iamRoleId, iamPolicyId])
}
model KmsAlias { model KmsAlias {
name String name String
accountId String accountId String

View File

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

View File

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

View File

@ -302,6 +302,28 @@ export enum Action {
SqsTagQueue = 'TagQueue', SqsTagQueue = 'TagQueue',
SqsUntagQueue = 'UntagQueue', 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 // STS
StsAssumeRole = 'AssumeRole', StsAssumeRole = 'AssumeRole',
StsAssumeRoleWithSaml = 'AssumeRoleWithSaml', StsAssumeRoleWithSaml = 'AssumeRoleWithSaml',

View File

@ -7,9 +7,10 @@ import * as js2xmlparser from 'js2xmlparser';
import { AbstractActionHandler, Format } from './abstract-action.handler'; import { AbstractActionHandler, Format } from './abstract-action.handler';
import { Action } from './action.enum'; import { Action } from './action.enum';
import { ActionHandlers } from './app.constants'; 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 { CommonConfig } from './config/common-config.interface';
import { InvalidAction, ValidationError } from './aws-shared-entities/aws-exceptions'; import { InvalidAction, ValidationError } from './aws-shared-entities/aws-exceptions';
import { IRequest } from './_context/request.context';
type QueryParams = { type QueryParams = {
__path: string; __path: string;
@ -28,7 +29,7 @@ export class AppController {
@HttpCode(200) @HttpCode(200)
@UseInterceptors(AuditInterceptor) @UseInterceptors(AuditInterceptor)
async post( async post(
@Req() request: Request, @Req() request: IRequest,
@Body() body: Record<string, any>, @Body() body: Record<string, any>,
@Headers() headers: Record<string, any>, @Headers() headers: Record<string, any>,
) { ) {
@ -56,15 +57,11 @@ export class AppController {
throw new ValidationError(validatorError.message); throw new ValidationError(validatorError.message);
} }
const awsProperties = { const jsonResponse = await handler.getResponse(validQueryParams, request.context);
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, awsProperties);
if (handler.format === Format.Xml) { 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; return jsonResponse;
} }

View File

@ -3,7 +3,7 @@ import { ConfigModule } from '@nestjs/config';
import { ActionHandlers } from './app.constants'; import { ActionHandlers } from './app.constants';
import { AppController } from './app.controller'; 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 { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
import localConfig from './config/local.config'; import localConfig from './config/local.config';
import { KMSHandlers } from './kms/kms.constants'; 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 { Action } from '../action.enum';
import { Format } from '../abstract-action.handler'; import { Format } from '../abstract-action.handler';
import { AwsException, InternalFailure } from '../aws-shared-entities/aws-exceptions'; 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() @Injectable()
@ -21,12 +22,20 @@ export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
@Inject(ActionHandlers) @Inject(ActionHandlers)
private readonly handlers: ActionHandlers, private readonly handlers: ActionHandlers,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly configService: ConfigService,
) {} ) {}
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> { 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 = { const requestContext: RequestContext = {
requestId: randomUUID(), requestId: randomUUID(),
awsProperties,
} }
const httpContext = context.switchToHttp(); 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 } }); 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 }[] = []; const pairs: { key: string, value: string }[] = [];
for (const param of Object.keys(queryParams)) { for (const param of Object.keys(queryParams)) {
const components = breakdownAwsQueryParam(param); 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 => ({ export const ExistingActionHandlersProvider = (inject: Array<InjectionToken | OptionalFactoryDependency>): Provider => ({
provide: ExistingActionHandlers, provide: ExistingActionHandlers,
useFactory: (...args: AbstractActionHandler[]) => args.reduce((m, h) => { 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; m[h.action] = h;
return m; return m;
}, {} as Record<Action, AbstractActionHandler>), }, {} as Record<Action, AbstractActionHandler>),

View File

@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { IamService } from './iam.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
PolicyArn: string; PolicyArn: string;
@ -12,7 +14,7 @@ type QueryParams = {
export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams> { export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams> {
constructor( constructor(
private readonly iamService: IamService,
) { ) {
super(); super();
} }
@ -24,8 +26,12 @@ export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams>
RoleName: Joi.string().required(), 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 * as Joi from 'joi';
import { IamPolicy } from './iam-policy.entity'; import { IamPolicy } from './iam-policy.entity';
import { breakdownArn } from '../util/breakdown-arn'; import { breakdownArn } from '../util/breakdown-arn';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
PolicyArn: string; PolicyArn: string;
@ -27,7 +28,7 @@ export class CreatePolicyVersionHandler extends AbstractActionHandler<QueryParam
SetAsDefault: Joi.boolean().required(), 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 { 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 { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { IamPolicy } from './iam-policy.entity';
import { IamService } from './iam.service'; import { IamService } from './iam.service';
import { TagsService } from '../aws-shared-entities/tags.service'; import { TagsService } from '../aws-shared-entities/tags.service';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
Description: string; Description: string;
@ -33,7 +33,7 @@ export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
PolicyName: Joi.string().min(1).max(128).required(), 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; const { Description, Path, PolicyName, PolicyDocument } = params;
@ -45,9 +45,11 @@ export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
path: Path, path: Path,
description: Description, description: Description,
policy: PolicyDocument, 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 * as Joi from 'joi';
import { IamService } from './iam.service'; import { IamService } from './iam.service';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
RoleName: string; RoleName: string;
@ -32,7 +33,7 @@ export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
RoleName: Joi.string().min(1).max(64).required(), 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({ const role = await this.iamService.createRole({
id: randomUUID(), id: randomUUID(),

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { IamService } from './iam.service'; import { IamService } from './iam.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
RoleName: string; RoleName: string;
@ -24,7 +25,7 @@ export class DeleteRoleHandler extends AbstractActionHandler<QueryParams> {
RoleName: Joi.string().min(1).max(64).required(), 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); 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
import { IamService } from './iam.service';
type QueryParams = { type QueryParams = {
PolicyArn: string; PolicyArn: string;
@ -12,7 +14,7 @@ type QueryParams = {
export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams> { export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams> {
constructor( constructor(
private readonly iamService: IamService,
) { ) {
super(); super();
} }
@ -24,7 +26,17 @@ export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams>
VersionId: Joi.string().required(), 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
import { IamService } from './iam.service';
type QueryParams = { type QueryParams = {
PolicyArn: string; PolicyArn: string;
@ -11,7 +13,7 @@ type QueryParams = {
export class GetPolicyHandler extends AbstractActionHandler<QueryParams> { export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
constructor( constructor(
private readonly iamService: IamService,
) { ) {
super(); super();
} }
@ -22,7 +24,10 @@ export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
PolicyArn: Joi.string().required(), 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 * as Joi from 'joi';
import { IamService } from './iam.service'; import { IamService } from './iam.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
RoleName: string; RoleName: string;
@ -24,14 +25,8 @@ export class GetRoleHandler extends AbstractActionHandler<QueryParams> {
RoleName: Joi.string().min(1).max(64).required(), 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); const role = await this.iamService.findOneRoleByName(awsProperties.accountId, RoleName);
if (!role) {
throw new NotFoundException();
}
return { return {
Role: role.metadata, Role: role.metadata,
} }

View File

@ -10,11 +10,19 @@ import { IAMHandlers } from './iam.constants';
import { PrismaModule } from '../_prisma/prisma.module'; import { PrismaModule } from '../_prisma/prisma.module';
import { IamService } from './iam.service'; import { IamService } from './iam.service';
import { GetRoleHandler } from './get-role.handler'; 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 = [ const handlers = [
AttachRolePolicyHandler,
CreatePolicyHandler, CreatePolicyHandler,
CreateRoleHandler, CreateRoleHandler,
GetPolicyVersionHandler,
GetPolicyHandler,
GetRoleHandler, GetRoleHandler,
ListAttachedRolePoliciesHandler,
] ]
const actions = [ const actions = [

View File

@ -4,7 +4,8 @@ import { PrismaService } from "../_prisma/prisma.service";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { IamPolicy } from "./iam-policy.entity"; import { IamPolicy } from "./iam-policy.entity";
import { IamRole } from "./iam-role.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() @Injectable()
export class IamService { export class IamService {
@ -22,15 +23,18 @@ export class IamService {
} }
} }
async findOneRoleByName(accountId: string, name: string): Promise<IamRole | null> { async findOneRoleByName(accountId: string, name: string): Promise<IamRole> {
const record = await this.prismaService.iamRole.findFirst({ try {
where: { const record = await this.prismaService.iamRole.findFirstOrThrow({
name, where: {
accountId, name,
} accountId,
}); }
});
return record ? new IamRole(record) : null; return new IamRole(record);
} catch (error) {
throw new NotFoundException();
}
} }
async deleteRoleByName(accountId: string, name: string) { async deleteRoleByName(accountId: string, name: string) {
@ -42,8 +46,84 @@ export class IamService {
}); });
} }
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> { async createPolicy(data: Prisma.IamPolicyCreateInput): Promise<IamPolicy> {
const record = await this.prismaService.iamPolicy.create({ data }); try {
return new IamPolicy(record); 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
import { IamService } from './iam.service';
type QueryParams = { type QueryParams = {
RoleName: string; RoleName: string;
@ -11,6 +13,7 @@ type QueryParams = {
export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<QueryParams> { export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
constructor( constructor(
private readonly iamService: IamService,
) { ) {
super(); super();
} }
@ -21,8 +24,15 @@ export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<Query
RoleName: Joi.string().required(), 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
Marker: string; Marker: string;
@ -25,7 +26,8 @@ export class ListRolePoliciesHandler extends AbstractActionHandler<QueryParams>
RoleName: Joi.string().required(), 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 * as Joi from 'joi';
import { KmsService } from './kms.service'; import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
AliasName: string; 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(), 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); 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 { keySpecToUsageType } from './kms-key.entity';
import { UnsupportedOperationException } from '../aws-shared-entities/aws-exceptions'; import { UnsupportedOperationException } from '../aws-shared-entities/aws-exceptions';
import { TagsService } from '../aws-shared-entities/tags.service'; 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]>> }; 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, }) 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; const keySpec = CustomerMasterKeySpec ?? KeySpec;

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { KmsService } from './kms.service'; import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
GrantTokens?: string[]; GrantTokens?: string[];
@ -26,7 +27,7 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
GrantTokens: Joi.array().items(Joi.string()), 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); 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 * as Joi from 'joi';
import { KmsService } from './kms.service'; import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
KeyId: string; KeyId: string;
@ -26,9 +27,9 @@ export class EnableKeyRotationHandler extends AbstractActionHandler<QueryParams>
RotationPeriodInDays: Joi.number().min(90).max(2560).default(365), 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) { if (!keyRecord) {
throw new NotFoundException(); throw new NotFoundException();

View File

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

View File

@ -4,6 +4,7 @@ import { Action } from '../action.enum';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { KmsService } from './kms.service'; import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
KeyId: string; KeyId: string;
@ -24,7 +25,7 @@ export class GetKeyRotationStatusHandler extends AbstractActionHandler<QueryPara
KeyId: Joi.string().required(), 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); 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

@ -59,6 +59,10 @@ export class KmsKey implements PrismaKmsKey {
return `arn:aws:kms:${this.region}:${this.accountId}:key/${this.id}`; 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() { get metadata() {
const dynamicContent: Record<string, any> = {}; const dynamicContent: Record<string, any> = {};

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { KmsService } from './kms.service'; import { KmsService } from './kms.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
KeyId?: string; KeyId?: string;
@ -28,7 +29,7 @@ export class ListAliasesHandler extends AbstractActionHandler<QueryParams> {
Marker: Joi.string(), 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 const records = await (KeyId
? this.kmsService.findAndCountAliasesByKeyId(awsProperties.accountId, awsProperties.region, Limit, KeyId, Marker) ? 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 { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions'; import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { TagsService } from '../aws-shared-entities/tags.service'; import { TagsService } from '../aws-shared-entities/tags.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
KeyId: string; KeyId: string;
@ -30,9 +31,9 @@ export class ListResourceTagsHandler extends AbstractActionHandler<QueryParams>
Marker: Joi.string(), 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) { if (!keyRecord) {
throw new NotFoundException(); 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); const app = await NestFactory.create(AppModule);
// app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); // app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.useGlobalFilters(new AwsExceptionFilter()); 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'})); app.use(bodyParser.json({ type: 'application/x-amz-json-1.1'}));
const configService: ConfigService<CommonConfig, true> = app.get(ConfigService); 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 { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
Name: string; Name: string;
@ -28,11 +29,11 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
validator = Joi.object<QueryParams, true>({ validator = Joi.object<QueryParams, true>({
Name: Joi.string().required(), Name: Joi.string().required(),
Description: Joi.string().allow('', null), Description: Joi.string().allow('', null),
SecretString: Joi.string().allow('', null), SecretString: Joi.string().allow('', null).default(''),
ClientRequestToken: Joi.string(), 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; const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params;
@ -41,8 +42,8 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
description, description,
name, name,
secretString, secretString,
accountId: awsProperties.accountId, accountId: context.awsProperties.accountId,
region: awsProperties.region, region: context.awsProperties.region,
}); });
const arn = ArnUtil.fromSecret(secret); const arn = ArnUtil.fromSecret(secret);

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -29,7 +30,7 @@ export class DeleteSecretHandler extends AbstractActionHandler {
VersionId: Joi.string().allow(null, ''), 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 name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = VersionId ? 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 * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; 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 { TagsService } from '../aws-shared-entities/tags.service';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -25,13 +27,13 @@ export class DescribeSecretHandler extends AbstractActionHandler {
action = Action.SecretsManagerDescribeSecret; action = Action.SecretsManagerDescribeSecret;
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() }); validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) { protected async handle({ SecretId }: QueryParams, { awsProperties} : RequestContext) {
const name = ArnUtil.getSecretNameFromSecretId(SecretId); const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
if (!secret) { 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); 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 { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -25,7 +26,7 @@ export class GetResourcePolicyHandler extends AbstractActionHandler {
action = Action.SecretsManagerGetResourcePolicy; action = Action.SecretsManagerGetResourcePolicy;
validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() }); validator = Joi.object<QueryParams, true>({ SecretId: Joi.string().required() });
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) { protected async handle({ SecretId }: QueryParams, { awsProperties} : RequestContext) {
const name = ArnUtil.getSecretNameFromSecretId(SecretId); const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); 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 { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -27,7 +28,7 @@ export class GetSecretValueHandler extends AbstractActionHandler {
VersionId: Joi.string().allow(null, ''), 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 name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = VersionId ? const secret = VersionId ?

View File

@ -6,6 +6,7 @@ import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -29,10 +30,10 @@ export class PutResourcePolicyHandler extends AbstractActionHandler {
ResourcePolicy: Joi.string().required(), 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 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) { if (!secret) {
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
SubscriptionArn: string; SubscriptionArn: string;
@ -25,7 +26,7 @@ export class GetSubscriptionAttributesHandler extends AbstractActionHandler {
action = Action.SnsGetSubscriptionAttributes; action = Action.SnsGetSubscriptionAttributes;
validator = Joi.object<QueryParams, true>({ SubscriptionArn: Joi.string().required() }); 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 id = SubscriptionArn.split(':').pop();
const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id }}); 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 { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
TopicArn: string; TopicArn: string;
@ -25,7 +26,7 @@ export class GetTopicAttributesHandler extends AbstractActionHandler {
action = Action.SnsGetTopicAttributes; action = Action.SnsGetTopicAttributes;
validator = Joi.object<QueryParams, true>({ TopicArn: Joi.string().required() }); 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 name = TopicArn.split(':').pop();
const topic = await this.prismaService.snsTopic.findFirst({ where: { name }}); 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { TagsService } from '../aws-shared-entities/tags.service'; import { TagsService } from '../aws-shared-entities/tags.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
ResourceArn: string; ResourceArn: string;
@ -22,7 +23,7 @@ export class ListTagsForResourceHandler extends AbstractActionHandler {
action = Action.SnsListTagsForResource; action = Action.SnsListTagsForResource;
validator = Joi.object<QueryParams, true>({ ResourceArn: Joi.string().required() }); 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); const tags = await this.tagsService.getByArn(ResourceArn);
return TagsService.getXmlSafeTagsMap(tags); 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
NextToken: number; NextToken: number;
@ -23,7 +24,7 @@ export class ListTopicsHandler extends AbstractActionHandler {
action = Action.SnsListTopics; action = Action.SnsListTopics;
validator = Joi.object<QueryParams, true>({ NextToken: Joi.number().default(0) }); 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([ const [ topics, total ] = await Promise.all([
this.prismaService.snsTopic.findMany({ orderBy: { name: 'desc' }, take: 100, skip }), 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 { SqsQueueEntryService } from '../sqs/sqs-queue-entry.service';
import { SqsQueue } from '../sqs/sqs-queue.entity'; import { SqsQueue } from '../sqs/sqs-queue.entity';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
TopicArn: string; TopicArn: string;
@ -37,7 +38,7 @@ export class PublishHandler extends AbstractActionHandler<QueryParams> {
Message: Joi.string().required(), 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; const arn = TopicArn ?? TargetArn;
if (!arn) { if (!arn) {
@ -65,7 +66,7 @@ export class PublishHandler extends AbstractActionHandler<QueryParams> {
SignatureVersion: topicAttributes.find(a => a.name === 'SignatureVersion')?.value ?? '1', SignatureVersion: topicAttributes.find(a => a.name === 'SignatureVersion')?.value ?? '1',
Signature: '', Signature: '',
SigningCertURL: '', SigningCertURL: '',
UnsubscribeURL: `${awsProperties.host}/?Action=Unsubscribe&SubscriptionArn=${subArn}`, UnsubscribeURL: `${context.awsProperties.host}/?Action=Unsubscribe&SubscriptionArn=${subArn}`,
}); });
await this.sqsQueueEntryService.publish(queueAccountId, queueName, message); 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
AttributeName: string; AttributeName: string;
@ -28,7 +29,7 @@ export class SetSubscriptionAttributesHandler extends AbstractActionHandler<Quer
TopicArn: Joi.string().required(), 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 }); 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
AttributeName: string; AttributeName: string;
@ -28,7 +29,7 @@ export class SetTopicAttributesHandler extends AbstractActionHandler<QueryParams
TopicArn: Joi.string().required(), 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 }); 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 * as Joi from 'joi';
import { PrismaService } from '../_prisma/prisma.service'; 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 { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { TagsService } from '../aws-shared-entities/tags.service'; import { TagsService } from '../aws-shared-entities/tags.service';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
TopicArn: string; TopicArn: string;
@ -34,7 +35,7 @@ export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
Protocol: Joi.string().required(), 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({ const subscription = await this.prismaService.snsTopicSubscription.create({
data: { data: {
@ -42,8 +43,8 @@ export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
topicArn: params.TopicArn, topicArn: params.TopicArn,
protocol: params.Protocol, protocol: params.Protocol,
endpoint: params.Endpoint, endpoint: params.Endpoint,
accountId: awsProperties.accountId, accountId: context.awsProperties.accountId,
region: awsProperties.region, region: context.awsProperties.region,
} }
}); });

View File

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

View File

@ -1,48 +1,18 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as Joi from 'joi'; 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 { Action } from '../action.enum';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { V2CreateQueueHandler } from './v2-create-queue.handler';
import { TagsService } from '../aws-shared-entities/tags.service';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity';
type QueryParams = { type QueryParams = {
QueueName: string; QueueName: string;
} }
@Injectable() @Injectable()
export class CreateQueueHandler extends AbstractActionHandler<QueryParams> { export class CreateQueueHandler extends V2CreateQueueHandler {
constructor(
private readonly sqsQueueEntryService: SqsQueueEntryService,
private readonly tagsService: TagsService,
private readonly attributeService: AttributesService,
) {
super();
}
format = Format.Xml; format = Format.Xml;
action = Action.SqsCreateQueue; action = Action.SqsCreateQueue;
validator = Joi.object<QueryParams, true>({ QueueName: Joi.string().required() }); 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 { Action } from '../action.enum';
import { SqsQueueEntryService } from './sqs-queue-entry.service'; import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity'; import { SqsQueue } from './sqs-queue.entity';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
QueueUrl: string; QueueUrl: string;
@ -26,7 +27,7 @@ export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams
QueueUrl: Joi.string().required(), QueueUrl: Joi.string().required(),
}); });
protected async handle( params : QueryParams, awsProperties: AwsProperties) { protected async handle( params : QueryParams, { awsProperties} : RequestContext) {
const { QueueUrl } = params; const { QueueUrl } = params;
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl); 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 { Action } from '../action.enum';
import { SqsQueueEntryService } from './sqs-queue-entry.service'; import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity'; import { SqsQueue } from './sqs-queue.entity';
import { RequestContext } from '../_context/request.context';
type QueryParams = { type QueryParams = {
QueueUrl: string; QueueUrl: string;
@ -28,7 +29,7 @@ export class DeleteMessageHandler extends AbstractActionHandler<QueryParams> {
ReceiptHandle: Joi.string().required(), 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); await this.sqsQueueEntryService.deleteMessage(ReceiptHandle);
} }
} }

View File

@ -31,7 +31,7 @@ export class DeleteQueueHandler extends AbstractActionHandler<QueryParams> {
__path: Joi.string().required(), __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 [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(params.QueueUrl ?? params.__path);
const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name); const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name);

View File

@ -31,7 +31,7 @@ export class GetQueueAttributesHandler extends AbstractActionHandler<QueryParams
__path: Joi.string().required(), __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 attributeNames = Object.keys(params).reduce((l, k) => {
const [name, _] = k.split('.'); const [name, _] = k.split('.');

View File

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

View File

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

View File

@ -30,7 +30,7 @@ export class ReceiveMessageHandler extends AbstractActionHandler<QueryParams> {
VisibilityTimeout: Joi.number(), 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 [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
const records = await this.sqsQueueEntryService.receiveMessages(accountId, name, MaxNumberOfMessages, VisibilityTimeout); 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(), __path: Joi.string().required(),
}); });
protected async handle(params: QueryParams, awsProperties: AwsProperties) { protected async handle(params: QueryParams) {
const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(params.__path); const [accountId, name] = SqsQueue.getAccountIdAndNameFromPath(params.__path);
const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name); const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name);
const attributes = SqsQueue.attributePairs(params); const attributes = SqsQueue.attributePairs(params);

View File

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

View File

@ -54,7 +54,12 @@ const attributeSlotMap = {
return SqsQueue.getAccountIdAndNameFromPath(workingString); 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 }[] = []; const pairs: { key: string, value: string }[] = [];
for (const param of Object.keys(queryParams)) { for (const param of Object.keys(queryParams)) {
const components = this.breakdownAwsQueryParam(param); const components = this.breakdownAwsQueryParam(param);

View File

@ -17,6 +17,8 @@ import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsHandlers } from './sqs.constants'; import { SqsHandlers } from './sqs.constants';
import { DeleteMessageBatchHandler } from './delete-message-batch.handler'; import { DeleteMessageBatchHandler } from './delete-message-batch.handler';
import { PrismaModule } from '../_prisma/prisma.module'; import { PrismaModule } from '../_prisma/prisma.module';
import { V2ListQueuesHandler } from './v2-list-queues.handler';
import { V2CreateQueueHandler } from './v2-create-queue.handler';
const handlers = [ const handlers = [
CreateQueueHandler, CreateQueueHandler,
@ -28,6 +30,8 @@ const handlers = [
PurgeQueueHandler, PurgeQueueHandler,
ReceiveMessageHandler, ReceiveMessageHandler,
SetQueueAttributesHandler, SetQueueAttributesHandler,
V2CreateQueueHandler,
V2ListQueuesHandler,
] ]
const actions = [ const actions = [
@ -51,6 +55,26 @@ const actions = [
Action.SqsSetQueueAttributes, Action.SqsSetQueueAttributes,
Action.SqsTagQueue, Action.SqsTagQueue,
Action.SqsUntagQueue, 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({ @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 { AbstractActionHandler, AwsProperties, Format } from "../abstract-action.handler";
import { Action } from "../action.enum"; import { Action } from "../action.enum";
import { RequestContext } from "../_context/request.context";
type QueryParams = {} type QueryParams = {}
@ -13,11 +14,11 @@ export class GetCallerIdentityHandler extends AbstractActionHandler<QueryParams>
action = Action.StsGetCallerIdentity; action = Action.StsGetCallerIdentity;
validator = Joi.object<QueryParams, true>(); validator = Joi.object<QueryParams, true>();
protected async handle(queryParams: QueryParams, awsProperties: AwsProperties) { protected async handle(queryParams: QueryParams, context: RequestContext) {
return { return {
"UserId": "AIDASAMPLEUSERID", "UserId": "AIDASAMPLEUSERID",
"Account": awsProperties.accountId, "Account": context.awsProperties.accountId,
"Arn": `arn:aws:iam::${awsProperties.accountId}:user/DevAdmin` "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;
}
}