Compare commits
7 Commits
main
...
prisma_mig
| Author | SHA1 | Date |
|---|---|---|
|
|
d8930a6a30 | |
|
|
da84b6b085 | |
|
|
1dc45267ac | |
|
|
c34ea76e4e | |
|
|
095ecbd643 | |
|
|
22da8d73d3 | |
|
|
a7fdedd310 |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
version: 3.7
|
|
||||||
services:
|
|
||||||
s3_provider:
|
|
||||||
image: minio
|
|
||||||
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
|
|
@ -10,49 +10,29 @@
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^9.3.10",
|
"@aws-sdk/client-kms": "^3.716.0",
|
||||||
"@nestjs/config": "^2.3.1",
|
"@nestjs/common": "^10.4.15",
|
||||||
"@nestjs/core": "^9.3.10",
|
"@nestjs/config": "^3.3.0",
|
||||||
"@nestjs/platform-express": "^9.3.10",
|
"@nestjs/core": "^10.4.15",
|
||||||
"@nestjs/typeorm": "^9.0.1",
|
"@nestjs/platform-express": "^10.4.15",
|
||||||
|
"@prisma/client": "^6.1.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
"execa": "^9.5.2",
|
||||||
"joi": "^17.9.0",
|
"joi": "^17.9.0",
|
||||||
"js2xmlparser": "^5.0.0",
|
"js2xmlparser": "^5.0.0",
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6"
|
||||||
"typeorm": "^0.3.12",
|
|
||||||
"uuidv4": "^6.2.13"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-sns": "^3.321.1",
|
"@nestjs/cli": "^10.4.9",
|
||||||
"@nestjs/cli": "^9.3.0",
|
|
||||||
"@nestjs/testing": "^9.4.0",
|
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.1",
|
"@types/joi": "^17.2.2",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/node": "^22.10.2",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"jest": "^29.5.0",
|
"prisma": "^6.1.0"
|
||||||
"supertest": "^6.3.3",
|
|
||||||
"ts-jest": "^29.1.0"
|
|
||||||
},
|
},
|
||||||
"jest": {
|
"engines": {
|
||||||
"globalSetup": "./_jest_/setup.ts",
|
"node": ">=22.11.0",
|
||||||
"globalTeardown": "./_jest_/teardown.ts",
|
"npm": ">=10.9.0"
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
|
||||||
"testRegex": ".*\\.*spec\\.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
|
||||||
"collectCoverageFrom": [
|
|
||||||
"**/*.(t|j)s"
|
|
||||||
],
|
|
||||||
"coverageDirectory": "../coverage",
|
|
||||||
"testEnvironment": "node"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "sqlite"
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:local-aws-state.sqlite"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Attribute {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
arn String
|
||||||
|
name String
|
||||||
|
value String
|
||||||
|
|
||||||
|
@@unique([arn, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Audit {
|
||||||
|
id String @id
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
action String?
|
||||||
|
request String?
|
||||||
|
response String?
|
||||||
|
}
|
||||||
|
|
||||||
|
model IamRole {
|
||||||
|
id String @id
|
||||||
|
path String?
|
||||||
|
name String
|
||||||
|
assumeRolePolicy String?
|
||||||
|
description String?
|
||||||
|
maxSessionDuration Int?
|
||||||
|
permissionBoundaryArn String?
|
||||||
|
lastUsedDate DateTime?
|
||||||
|
lastUsedRegion String?
|
||||||
|
accountId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
policies IamRoleIamPolicyAttachment[]
|
||||||
|
|
||||||
|
@@unique([accountId, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model IamPolicy {
|
||||||
|
id String
|
||||||
|
version Int @default(1)
|
||||||
|
isDefault Boolean
|
||||||
|
path String?
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
policy String
|
||||||
|
isAttachable Boolean @default(false)
|
||||||
|
accountId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@id([id, version])
|
||||||
|
@@unique([accountId, path, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model IamRoleIamPolicyAttachment {
|
||||||
|
iamRoleId String
|
||||||
|
iamPolicyId String
|
||||||
|
|
||||||
|
role IamRole @relation(fields: [iamRoleId], references: [id])
|
||||||
|
|
||||||
|
@@id([iamRoleId, iamPolicyId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model KmsAlias {
|
||||||
|
name String
|
||||||
|
accountId String
|
||||||
|
region String
|
||||||
|
kmsKeyId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
kmsKey KmsKey @relation(fields: [kmsKeyId], references: [id])
|
||||||
|
|
||||||
|
@@id([accountId, region, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model KmsKey {
|
||||||
|
id String @id
|
||||||
|
enabled Boolean
|
||||||
|
usage String
|
||||||
|
description String
|
||||||
|
keySpec String
|
||||||
|
keyState String
|
||||||
|
origin String
|
||||||
|
multiRegion Boolean
|
||||||
|
policy String
|
||||||
|
key Bytes
|
||||||
|
rotationPeriod Int?
|
||||||
|
nextRotation DateTime?
|
||||||
|
accountId String
|
||||||
|
region String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
aliases KmsAlias[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Secret {
|
||||||
|
versionId String @id
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
secretString String
|
||||||
|
accountId String
|
||||||
|
region String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
deletionDate DateTime?
|
||||||
|
|
||||||
|
@@index([name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model SnsTopic {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
accountId String
|
||||||
|
region String
|
||||||
|
|
||||||
|
@@unique([accountId, region, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model SnsTopicSubscription {
|
||||||
|
id String @id
|
||||||
|
topicArn String
|
||||||
|
endpoint String?
|
||||||
|
protocol String
|
||||||
|
accountId String
|
||||||
|
region String
|
||||||
|
}
|
||||||
|
|
||||||
|
model SqsQueue {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
accountId String
|
||||||
|
region String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
messages SqsQueueMessage[]
|
||||||
|
|
||||||
|
@@unique([accountId, region, name])
|
||||||
|
}
|
||||||
|
|
||||||
|
model SqsQueueMessage {
|
||||||
|
id String @id
|
||||||
|
queueId Int
|
||||||
|
senderId String
|
||||||
|
message String
|
||||||
|
inFlightRelease DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
queue SqsQueue @relation(fields: [queueId], references: [id])
|
||||||
|
|
||||||
|
@@index([queueId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tag {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
arn String
|
||||||
|
name String
|
||||||
|
value String
|
||||||
|
|
||||||
|
@@unique([arn, name])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ArgumentsHost, Catch, ExceptionFilter } from "@nestjs/common";
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
import { AwsException } from "../aws-shared-entities/aws-exceptions";
|
||||||
|
import { IRequest } from "./request.context";
|
||||||
|
import { Format } from "../abstract-action.handler";
|
||||||
|
|
||||||
|
@Catch(AwsException)
|
||||||
|
export class AwsExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(exception: AwsException, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const request = ctx.getRequest<IRequest>();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
|
||||||
|
exception.requestId = request.context.requestId;
|
||||||
|
|
||||||
|
if (request.context.format === Format.Xml) {
|
||||||
|
const xml = exception.toXml();
|
||||||
|
return response.status(exception.statusCode).send(xml);
|
||||||
|
}
|
||||||
|
const [newError, newHeaders] = exception.toJson();
|
||||||
|
response.setHeaders(new Map(Object.entries(newHeaders)));
|
||||||
|
return response.status(exception.statusCode).json(newError.getResponse());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Request } from "express";
|
||||||
|
|
||||||
|
import { Action } from "../action.enum";
|
||||||
|
import { AwsProperties, Format } from "../abstract-action.handler";
|
||||||
|
|
||||||
|
export interface RequestContext {
|
||||||
|
action?: Action;
|
||||||
|
format?: Format;
|
||||||
|
awsProperties: AwsProperties;
|
||||||
|
readonly requestId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest extends Request {
|
||||||
|
context: RequestContext;
|
||||||
|
headers: {
|
||||||
|
'x-amz-target'?: string;
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
'Action'?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { INestApplication } from '@nestjs/common';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { AppModule } from '../app.module';
|
|
||||||
|
|
||||||
const globalSetup = async (_globalConfig, _projectConfig) => {
|
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AppModule],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
const app: INestApplication = module.createNestApplication();
|
|
||||||
await app.listen(4566);
|
|
||||||
|
|
||||||
globalThis.__TESTMODULE__ = module;
|
|
||||||
globalThis.__NESTAPP__ = app;
|
|
||||||
globalThis.__ENDPOINT__ = 'http://127.0.0.1:4566';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default globalSetup;
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { INestApplication } from '@nestjs/common';
|
|
||||||
|
|
||||||
const globalTeardown = async (_globalConfig, _projectConfig) => {
|
|
||||||
|
|
||||||
await (globalThis.__NESTAPP__ as INestApplication).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default globalTeardown;
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "./prisma.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
providers: [PrismaService],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { OnModuleInit } from "@nestjs/common";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
import { Action } from './action.enum';
|
import { Action } from './action.enum';
|
||||||
import * as uuid from 'uuid';
|
|
||||||
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,42 +18,43 @@ 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/"
|
||||||
},
|
},
|
||||||
ResponseMetadata: {
|
ResponseMetadata: {
|
||||||
RequestId: uuid.v4(),
|
RequestId: randomUUID(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,4 +301,37 @@ export enum Action {
|
||||||
SqsSetQueueAttributes = 'SetQueueAttributes',
|
SqsSetQueueAttributes = 'SetQueueAttributes',
|
||||||
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
|
||||||
|
StsAssumeRole = 'AssumeRole',
|
||||||
|
StsAssumeRoleWithSaml = 'AssumeRoleWithSaml',
|
||||||
|
StsAssumeRoleWithWebIdentity = 'AssumeRoleWithWebIdentity',
|
||||||
|
StsAssumeRoot = 'AssumeRoot',
|
||||||
|
StsDecodeAuthorizationMessage = 'DecodeAuthorizationMessage',
|
||||||
|
StsGetAccessKeyInfo = 'GetAccessKeyInfo',
|
||||||
|
StsGetCallerIdentity = 'GetCallerIdentity',
|
||||||
|
StsGetFederationToken = 'GetFederationToken',
|
||||||
|
StsGetSessionToken = 'GetSessionToken',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
import { BadRequestException, Body, Controller, Inject, Post, Headers, Req, HttpCode, UseInterceptors } from '@nestjs/common';
|
import { BadRequestException, Body, Controller, Headers, HttpCode, Inject, Post, Req, UseInterceptors } from '@nestjs/common';
|
||||||
import { ActionHandlers } from './app.constants';
|
|
||||||
import * as Joi from 'joi';
|
|
||||||
import { Action } from './action.enum';
|
|
||||||
import { AbstractActionHandler, Format } from './abstract-action.handler';
|
|
||||||
import * as js2xmlparser from 'js2xmlparser';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { CommonConfig } from './config/common-config.interface';
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import * as js2xmlparser from 'js2xmlparser';
|
||||||
|
|
||||||
|
import { AbstractActionHandler, Format } from './abstract-action.handler';
|
||||||
|
import { Action } from './action.enum';
|
||||||
|
import { ActionHandlers } from './app.constants';
|
||||||
import { AuditInterceptor } from './audit/audit.interceptor';
|
import { AuditInterceptor } from './audit/audit.interceptor';
|
||||||
|
import { CommonConfig } from './config/common-config.interface';
|
||||||
|
import { InvalidAction, ValidationError } from './aws-shared-entities/aws-exceptions';
|
||||||
|
import { IRequest } from './_context/request.context';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
__path: string;
|
||||||
|
} & Record<string, string>;
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
|
|
@ -22,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>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -30,35 +37,31 @@ export class AppController {
|
||||||
const lowerCasedHeaders = Object.keys(headers).reduce((o, k) => {
|
const lowerCasedHeaders = Object.keys(headers).reduce((o, k) => {
|
||||||
o[k.toLocaleLowerCase()] = headers[k];
|
o[k.toLocaleLowerCase()] = headers[k];
|
||||||
return o;
|
return o;
|
||||||
}, {})
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
const queryParams = { __path: request.path, ...body, ...lowerCasedHeaders };
|
const queryParams: QueryParams = { __path: request.path, ...body, ...lowerCasedHeaders };
|
||||||
const actionKey = queryParams['x-amz-target'] ? 'x-amz-target' : 'Action';
|
const actionKey = queryParams['x-amz-target'] ? 'x-amz-target' : 'Action';
|
||||||
const { error: actionError } = Joi.object({
|
const { error: actionError } = Joi.object({
|
||||||
[actionKey]: Joi.string().valid(...Object.values(Action)).required(),
|
[actionKey]: Joi.string().valid(...Object.values(Action)).required(),
|
||||||
}).validate(queryParams, { allowUnknown: true });
|
}).validate(queryParams, { allowUnknown: true });
|
||||||
|
|
||||||
if (actionError) {
|
if (actionError) {
|
||||||
throw new BadRequestException(actionError.message, { cause: actionError });
|
throw new InvalidAction(actionError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = queryParams[actionKey];
|
const action = queryParams[actionKey] as Action;
|
||||||
const handler: AbstractActionHandler = this.actionHandlers[action];
|
const handler: AbstractActionHandler = this.actionHandlers[action];
|
||||||
const { error: validatorError, value: validQueryParams } = handler.validator.validate(queryParams, { allowUnknown: true, abortEarly: false });
|
const { error: validatorError, value: validQueryParams } = handler.validator.validate(queryParams, { allowUnknown: true, abortEarly: false });
|
||||||
|
|
||||||
if (validatorError) {
|
if (validatorError) {
|
||||||
throw new BadRequestException(validatorError.message, { cause: validatorError });
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { ActionHandlers } from './app.constants';
|
import { ActionHandlers } from './app.constants';
|
||||||
import { CommonConfig } from './config/common-config.interface';
|
import { AppController } from './app.controller';
|
||||||
|
import { AuditInterceptor } from './audit/audit.interceptor';
|
||||||
|
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 { KmsModule } from './kms/kms.module';
|
||||||
|
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
|
||||||
|
import { SecretsManagerModule } from './secrets-manager/secrets-manager.module';
|
||||||
import { SnsHandlers } from './sns/sns.constants';
|
import { SnsHandlers } from './sns/sns.constants';
|
||||||
import { SnsModule } from './sns/sns.module';
|
import { SnsModule } from './sns/sns.module';
|
||||||
import { AppController } from './app.controller';
|
|
||||||
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
|
|
||||||
import { SecretsManagerModule } from './secrets-manager/secrets-manager.module';
|
|
||||||
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
|
|
||||||
import { SqsModule } from './sqs/sqs.module';
|
|
||||||
import { SqsHandlers } from './sqs/sqs.constants';
|
import { SqsHandlers } from './sqs/sqs.constants';
|
||||||
import { Audit } from './audit/audit.entity';
|
import { SqsModule } from './sqs/sqs.module';
|
||||||
import { AuditInterceptor } from './audit/audit.interceptor';
|
import { PrismaModule } from './_prisma/prisma.module';
|
||||||
import { KmsModule } from './kms/kms.module';
|
import { StsModule } from './sts/sts.module';
|
||||||
import { KMSHandlers } from './kms/kms.constants';
|
import { StsHandlers } from './sts/sts.constants';
|
||||||
import { configValidator } from './config/config.validator';
|
|
||||||
import { IamModule } from './iam/iam.module';
|
import { IamModule } from './iam/iam.module';
|
||||||
import { IAMHandlers } from './iam/iam.constants';
|
import { IAMHandlers } from './iam/iam.constants';
|
||||||
|
|
||||||
|
|
@ -26,23 +26,14 @@ import { IAMHandlers } from './iam/iam.constants';
|
||||||
load: [localConfig],
|
load: [localConfig],
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forRootAsync({
|
PrismaModule,
|
||||||
inject: [ConfigService],
|
AwsSharedEntitiesModule,
|
||||||
useFactory: (configService: ConfigService<CommonConfig>) => ({
|
|
||||||
type: 'sqlite',
|
|
||||||
database: configService.get('DB_DATABASE') === ':memory:' ? configService.get('DB_DATABASE') : `${__dirname}/../data/${configService.get('DB_DATABASE')}`,
|
|
||||||
logging: configService.get('DB_LOGGING'),
|
|
||||||
synchronize: configService.get('DB_SYNCHRONIZE'),
|
|
||||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
TypeOrmModule.forFeature([Audit]),
|
|
||||||
IamModule,
|
IamModule,
|
||||||
KmsModule,
|
KmsModule,
|
||||||
SecretsManagerModule,
|
SecretsManagerModule,
|
||||||
SnsModule,
|
SnsModule,
|
||||||
SqsModule,
|
SqsModule,
|
||||||
AwsSharedEntitiesModule,
|
StsModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
AppController,
|
AppController,
|
||||||
|
|
@ -53,11 +44,12 @@ import { IAMHandlers } from './iam/iam.constants';
|
||||||
provide: ActionHandlers,
|
provide: ActionHandlers,
|
||||||
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
|
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
|
||||||
inject: [
|
inject: [
|
||||||
|
IAMHandlers,
|
||||||
|
KMSHandlers,
|
||||||
|
SecretsManagerHandlers,
|
||||||
SnsHandlers,
|
SnsHandlers,
|
||||||
SqsHandlers,
|
SqsHandlers,
|
||||||
SecretsManagerHandlers,
|
StsHandlers,
|
||||||
KMSHandlers,
|
|
||||||
IAMHandlers,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Controller } from "@nestjs/common";
|
||||||
|
import { AuditService } from "./audit.service";
|
||||||
|
|
||||||
|
@Controller('_audit')
|
||||||
|
export class AuditController {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly auditService: AuditService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('audit')
|
|
||||||
export class Audit extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn()
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
action: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
request: string;
|
|
||||||
|
|
||||||
@Column({ nullable: true })
|
|
||||||
response: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +1,110 @@
|
||||||
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
|
import { CallHandler, ExecutionContext, HttpException, Inject, Injectable, Logger, NestInterceptor, RequestTimeoutException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { randomUUID } from 'crypto';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { catchError, Observable, tap, throwError } from 'rxjs';
|
||||||
import { Repository } from 'typeorm';
|
import { Request as ExpressRequest, Response } from 'express';
|
||||||
import { Audit } from './audit.entity';
|
import * as Joi from 'joi';
|
||||||
import * as uuid from 'uuid';
|
|
||||||
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
import { ActionHandlers } from '../app.constants';
|
import { ActionHandlers } from '../app.constants';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { Format } from '../abstract-action.handler';
|
||||||
|
import { AwsException, InternalFailure } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
import { IRequest, RequestContext } from '../_context/request.context';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
|
export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
|
||||||
|
|
||||||
|
private readonly logger = new Logger(AuditInterceptor.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Audit)
|
|
||||||
private readonly auditRepo: Repository<Audit>,
|
|
||||||
@Inject(ActionHandlers)
|
@Inject(ActionHandlers)
|
||||||
private readonly handlers: ActionHandlers,
|
private readonly handlers: ActionHandlers,
|
||||||
|
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 requestId = uuid.v4();
|
const requestContext: RequestContext = {
|
||||||
const httpContext = context.switchToHttp();
|
requestId: randomUUID(),
|
||||||
|
awsProperties,
|
||||||
const request = httpContext.getRequest();
|
|
||||||
const targetHeaderKey = Object.keys(request.headers).find( k => k.toLocaleLowerCase() === 'x-amz-target');
|
|
||||||
const action = request.headers[targetHeaderKey] ? request.headers[targetHeaderKey] : request.body.Action;
|
|
||||||
|
|
||||||
const response = context.switchToHttp().getResponse();
|
|
||||||
|
|
||||||
response.header('x-amzn-RequestId', requestId);
|
|
||||||
|
|
||||||
if (!this.handlers[action]?.audit) {
|
|
||||||
return next.handle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const httpContext = context.switchToHttp();
|
||||||
|
const request = httpContext.getRequest<IRequest>();
|
||||||
|
request.context = requestContext;
|
||||||
|
|
||||||
|
const hasTargetHeader = Object.keys(request.headers).some( k => k.toLocaleLowerCase() === 'x-amz-target');
|
||||||
|
const action = hasTargetHeader ? request.headers['x-amz-target'] : request.body.Action;
|
||||||
|
const { value: resolvedAction } = Joi.string().required().valid(...Object.values(Action)).validate(action) as { value: Action | undefined };
|
||||||
|
requestContext.action = resolvedAction;
|
||||||
|
|
||||||
|
const response = context.switchToHttp().getResponse<Response>();
|
||||||
|
response.header('x-amzn-RequestId', requestContext.requestId);
|
||||||
|
|
||||||
|
if (!resolvedAction || !this.handlers[resolvedAction]?.audit) {
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
tap({
|
catchError(async (error: Error) => {
|
||||||
|
await this.prismaService.audit.create({
|
||||||
next: async (data) => await this.auditRepo.create({
|
data: {
|
||||||
id: requestId,
|
id: requestContext.requestId,
|
||||||
action,
|
|
||||||
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
|
|
||||||
response: JSON.stringify(data),
|
|
||||||
}).save(),
|
|
||||||
|
|
||||||
error: async (error) => await this.auditRepo.create({
|
|
||||||
id: requestId,
|
|
||||||
action,
|
action,
|
||||||
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
|
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
|
||||||
response: JSON.stringify(error),
|
response: JSON.stringify(error),
|
||||||
}).save(),
|
}
|
||||||
|
});
|
||||||
|
this.logger.error(error.message);
|
||||||
|
return error;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = this.handlers[resolvedAction];
|
||||||
|
requestContext.format = handler.format;
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
catchError((error: Error) => {
|
||||||
|
|
||||||
|
return throwError(() => {
|
||||||
|
|
||||||
|
if (error instanceof AwsException) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultError = new InternalFailure('Unexpected local AWS exception...');
|
||||||
|
this.logger.error(error.message);
|
||||||
|
defaultError.requestId = requestContext.requestId;
|
||||||
|
return defaultError;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
tap({
|
||||||
|
|
||||||
|
next: async (data) => await this.prismaService.audit.create({
|
||||||
|
data: {
|
||||||
|
id: requestContext.requestId,
|
||||||
|
action,
|
||||||
|
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
|
||||||
|
response: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
error: async (error) => await this.prismaService.audit.create({
|
||||||
|
data: {
|
||||||
|
id: requestContext.requestId,
|
||||||
|
action,
|
||||||
|
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
|
||||||
|
response: JSON.stringify(error),
|
||||||
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaService } from "../_prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuditService {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { BaseEntity, Column, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('attributes')
|
|
||||||
export class Attribute extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryGeneratedColumn({ name: 'id' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'arn', nullable: false })
|
|
||||||
@Index()
|
|
||||||
arn: string;
|
|
||||||
|
|
||||||
@Column({ name: 'name', nullable: false })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ name: 'value', nullable: false })
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { Attribute, Prisma } from '@prisma/client';
|
||||||
import { In, Repository } from 'typeorm';
|
|
||||||
import { Attribute } from './attributes.entity';
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
import { CreateAttributeDto } from './create-attribute.dto';
|
import { breakdownAwsQueryParam } from '../util/breakdown-aws-query-param';
|
||||||
|
|
||||||
const ResourcePolicyName = 'ResourcePolicy';
|
const ResourcePolicyName = 'ResourcePolicy';
|
||||||
|
|
||||||
|
|
@ -10,62 +10,75 @@ const ResourcePolicyName = 'ResourcePolicy';
|
||||||
export class AttributesService {
|
export class AttributesService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Attribute)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly repo: Repository<Attribute>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getByArn(arn: string): Promise<Attribute[]> {
|
async getByArn(arn: string): Promise<Attribute[]> {
|
||||||
return await this.repo.find({ where: { arn }});
|
return await this.prismaService.attribute.findMany({ where: { arn }});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourcePolicyByArn(arn: string): Promise<Attribute> {
|
async getResourcePolicyByArn(arn: string): Promise<Attribute | null> {
|
||||||
return await this.repo.findOne({ where: { arn, name: ResourcePolicyName }});
|
return await this.prismaService.attribute.findFirst({ where: { arn, name: ResourcePolicyName }});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByArnAndName(arn: string, name: string): Promise<Attribute> {
|
async getByArnAndName(arn: string, name: string): Promise<Attribute | null> {
|
||||||
return await this.repo.findOne({ where: { arn, name }});
|
return await this.prismaService.attribute.findFirst({ where: { arn, name }});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByArnAndNames(arn: string, names: string[]): Promise<Attribute[]> {
|
async getByArnAndNames(arn: string, names: string[]): Promise<Attribute[]> {
|
||||||
return await this.repo.find({ where: { arn, name: In(names) }});
|
return await this.prismaService.attribute.findMany({ where: {
|
||||||
|
arn,
|
||||||
|
name: {
|
||||||
|
in: names
|
||||||
|
}
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createResourcePolicy(arn: string, value: string): Promise<Attribute> {
|
async createResourcePolicy(arn: string, value: string): Promise<Attribute> {
|
||||||
return await this.create({arn, value, name: ResourcePolicyName });
|
return await this.create({arn, value, name: ResourcePolicyName });
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateAttributeDto): Promise<Attribute> {
|
async create(data: Prisma.AttributeCreateArgs['data']): Promise<Attribute> {
|
||||||
return await this.repo.save(dto);
|
return await this.prismaService.attribute.create({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteByArn(arn: string) {
|
async deleteByArn(arn: string): Promise<void> {
|
||||||
await this.repo.delete({ arn });
|
await this.prismaService.attribute.deleteMany({ where: { arn } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteByArnAndName(arn: string, name: string) {
|
async deleteByArnAndName(arn: string, name: string): Promise<void> {
|
||||||
await this.repo.delete({ arn, name });
|
await this.prismaService.attribute.deleteMany({ where: { arn, name } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMany(arn: string, records: { key: string, value: string }[]): Promise<void> {
|
async createMany(arn: string, records: { key: string, value: string }[]): Promise<void> {
|
||||||
for (const record of records) {
|
await this.prismaService.attribute.createMany({
|
||||||
await this.create({ arn, name: record.key, value: record.value });
|
data: records.map(r => ({
|
||||||
}
|
name: r.key,
|
||||||
|
value: r.value,
|
||||||
|
arn,
|
||||||
|
}))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static attributePairs(queryParams: Record<string, string>): { key: string, value: string }[] {
|
static attributePairs(queryParams: Record<string, string>): { key: string, value: string }[] {
|
||||||
const pairs = [null];
|
const pairs: { key: string, value: string }[] = [];
|
||||||
for (const param of Object.keys(queryParams)) {
|
for (const param of Object.keys(queryParams)) {
|
||||||
const [type, _, idx, slot] = param.split('.');
|
const components = breakdownAwsQueryParam(param);
|
||||||
|
|
||||||
|
if (!components) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [type, _, idx, slot] = components;
|
||||||
|
|
||||||
if (type === 'Attributes') {
|
if (type === 'Attributes') {
|
||||||
if (!pairs[+idx]) {
|
if (!pairs[idx]) {
|
||||||
pairs[+idx] = { key: '', value: ''};
|
pairs[idx] = { key: '', value: ''};
|
||||||
}
|
}
|
||||||
pairs[+idx][slot] = queryParams[param];
|
pairs[idx][slot] = queryParams[param];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pairs.shift();
|
|
||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { HttpException, HttpStatus } from "@nestjs/common";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import * as js2xmlparser from 'js2xmlparser';
|
||||||
|
|
||||||
|
export abstract class AwsException {
|
||||||
|
|
||||||
|
requestId: string = randomUUID();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly message: string,
|
||||||
|
readonly errorType: string,
|
||||||
|
readonly statusCode: HttpStatus,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
toXml(): string {
|
||||||
|
return js2xmlparser.parse(`ErrorResponse`, {
|
||||||
|
RequestId: this.requestId,
|
||||||
|
Error: {
|
||||||
|
Code: this.errorType,
|
||||||
|
Message: this.message,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): [HttpException, Record<string, string>] {
|
||||||
|
return [
|
||||||
|
new HttpException({
|
||||||
|
message: this.message,
|
||||||
|
__type: this.errorType,
|
||||||
|
}, this.statusCode),
|
||||||
|
{
|
||||||
|
'Server': 'NestJS/local-aws',
|
||||||
|
'X-Amzn-Errortype': this.errorType,
|
||||||
|
'x-amzn-requestid': this.requestId,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccessDeniedException extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
AccessDeniedException.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class IncompleteSignature extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
IncompleteSignature.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class InternalFailure extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
InternalFailure.name,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class InvalidAction extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
InvalidAction.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class InvalidClientTokenId extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
InvalidClientTokenId.name,
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class NotAuthorized extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
NotAuthorized.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class OptInRequired extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
OptInRequired.name,
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class RequestExpired extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
RequestExpired.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ServiceUnavailable extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
ServiceUnavailable.name,
|
||||||
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ThrottlingException extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
ThrottlingException.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ValidationError extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
ValidationError.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class NotFoundException extends AwsException {
|
||||||
|
constructor() {
|
||||||
|
super(
|
||||||
|
'The request was rejected because the specified entity or resource could not be found.',
|
||||||
|
NotFoundException.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class InvalidArnException extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
InvalidArnException.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class UnsupportedOperationException extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
UnsupportedOperationException.name,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class EntityAlreadyExists extends AwsException {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(
|
||||||
|
message,
|
||||||
|
EntityAlreadyExists.name,
|
||||||
|
HttpStatus.CONFLICT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Attribute } from './attributes.entity';
|
|
||||||
import { AttributesService } from './attributes.service';
|
import { AttributesService } from './attributes.service';
|
||||||
import { Tag } from './tags.entity';
|
|
||||||
import { TagsService } from './tags.service';
|
import { TagsService } from './tags.service';
|
||||||
|
import { PrismaModule } from '../_prisma/prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Attribute, Tag])],
|
imports: [PrismaModule],
|
||||||
providers: [AttributesService, TagsService],
|
providers: [AttributesService, TagsService],
|
||||||
exports: [AttributesService, TagsService],
|
exports: [AttributesService, TagsService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export interface CreateAttributeDto {
|
|
||||||
arn: string;
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export interface CreateTagDto {
|
|
||||||
arn: string;
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { BaseEntity, Column, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('tags')
|
|
||||||
export class Tag extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryGeneratedColumn({ name: 'id' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'arn', nullable: false})
|
|
||||||
@Index()
|
|
||||||
arn: string;
|
|
||||||
|
|
||||||
@Column({ name: 'name', nullable: false })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ name: 'value', nullable: false })
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +1,61 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { Prisma, Tag } from '@prisma/client';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { Tag } from './tags.entity';
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
import { CreateTagDto } from './create-tag.dto';
|
import { breakdownAwsQueryParam } from '../util/breakdown-aws-query-param';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TagsService {
|
export class TagsService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Tag)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly repo: Repository<Tag>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getByArn(arn: string): Promise<Tag[]> {
|
async getByArn(arn: string): Promise<Tag[]> {
|
||||||
return await this.repo.find({ where: { arn }});
|
return await this.prismaService.tag.findMany({ where: { arn }});
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateTagDto): Promise<Tag> {
|
async create(data: Prisma.TagCreateArgs['data']): Promise<Tag> {
|
||||||
return await this.repo.save(dto);
|
return await this.prismaService.tag.create({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMany(arn: string, records: { Key: string, Value: string }[]): Promise<void> {
|
async createMany(arn: string, records: { key: string, value: string }[]): Promise<void> {
|
||||||
for (const record of records) {
|
await this.prismaService.tag.createMany({
|
||||||
await this.create({ arn, name: record.Key, value: record.Value });
|
data: records.map(r => ({
|
||||||
}
|
name: r.key,
|
||||||
|
value: r.value,
|
||||||
|
arn,
|
||||||
|
}))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteByArn(arn: string) {
|
async deleteByArn(arn: string): Promise<void> {
|
||||||
await this.repo.delete({ arn });
|
await this.prismaService.tag.deleteMany({ where: { arn } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteByArnAndName(arn: string, name: string) {
|
async deleteByArnAndName(arn: string, name: string): Promise<void> {
|
||||||
await this.repo.delete({ 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 = [null];
|
const pairs: { key: string, value: string }[] = [];
|
||||||
for (const param of Object.keys(queryParams)) {
|
for (const param of Object.keys(queryParams)) {
|
||||||
const [type, _, idx, slot] = param.split('.');
|
const components = breakdownAwsQueryParam(param);
|
||||||
|
|
||||||
|
if (!components) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [type, _, idx, slot] = components;
|
||||||
|
|
||||||
if (type === 'Tags') {
|
if (type === 'Tags') {
|
||||||
if (!pairs[+idx]) {
|
if (!pairs[+idx]) {
|
||||||
pairs[+idx] = { Key: '', Value: ''};
|
pairs[+idx] = { key: '', value: ''};
|
||||||
}
|
}
|
||||||
pairs[+idx][slot] = queryParams[param];
|
pairs[+idx][slot] = queryParams[param];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pairs.shift();
|
|
||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Provider } from '@nestjs/common';
|
import { InjectionToken, Provider } from '@nestjs/common';
|
||||||
import { Action } from '../action.enum';
|
import { Action } from '../action.enum';
|
||||||
import { ExistingActionHandlers } from './default-action-handler.constants';
|
import { ExistingActionHandlers } from './default-action-handler.constants';
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import { AbstractActionHandler, Format } from '../abstract-action.handler';
|
import { AbstractActionHandler, Format } from '../abstract-action.handler';
|
||||||
|
|
||||||
export const DefaultActionHandlerProvider = (symbol, format: Format, actions: Action[]): Provider => ({
|
export const DefaultActionHandlerProvider = (symbol: InjectionToken, format: Format, actions: Action[]): Provider => ({
|
||||||
provide: symbol,
|
provide: symbol,
|
||||||
useFactory: (existingActionHandlers: ExistingActionHandlers) => {
|
useFactory: (existingActionHandlers: ExistingActionHandlers) => {
|
||||||
const cloned = { ...existingActionHandlers };
|
const cloned = { ...existingActionHandlers };
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,22 @@
|
||||||
import { Provider } from '@nestjs/common';
|
import { InjectionToken, OptionalFactoryDependency, Provider } from '@nestjs/common';
|
||||||
|
|
||||||
import { AbstractActionHandler } from '../abstract-action.handler';
|
import { AbstractActionHandler } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
import { ExistingActionHandlers } from './default-action-handler.constants';
|
import { ExistingActionHandlers } from './default-action-handler.constants';
|
||||||
|
|
||||||
export const ExistingActionHandlersProvider = (inject): 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>),
|
||||||
inject,
|
inject,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,12 +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 { InjectRepository } from '@nestjs/typeorm';
|
import { IamService } from './iam.service';
|
||||||
import { Repository } from 'typeorm';
|
import { RequestContext } from '../_context/request.context';
|
||||||
import * as uuid from 'uuid';
|
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
|
|
||||||
import { IamRole } from './iam-role.entity';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
PolicyArn: string;
|
PolicyArn: string;
|
||||||
|
|
@ -18,10 +14,7 @@ type QueryParams = {
|
||||||
export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams> {
|
export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamRole)
|
private readonly iamService: IamService,
|
||||||
private readonly roleRepo: Repository<IamRole>,
|
|
||||||
@InjectRepository(IamRolePolicyAttachment)
|
|
||||||
private readonly attachRepo: Repository<IamRolePolicyAttachment>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -33,15 +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) {
|
||||||
|
|
||||||
const role = await this.roleRepo.findOne({ where: { roleName: RoleName, accountId: awsProperties.accountId} });
|
await this.iamService.attachPolicyToRoleName(
|
||||||
|
context.awsProperties.accountId,
|
||||||
await this.attachRepo.create({
|
PolicyArn,
|
||||||
id: uuid.v4(),
|
RoleName
|
||||||
policyArn: PolicyArn,
|
);
|
||||||
roleId: role.id,
|
|
||||||
accountId: awsProperties.accountId,
|
|
||||||
}).save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@ 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 { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import * as uuid from 'uuid';
|
|
||||||
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;
|
||||||
|
|
@ -18,8 +16,6 @@ type QueryParams = {
|
||||||
export class CreatePolicyVersionHandler extends AbstractActionHandler<QueryParams> {
|
export class CreatePolicyVersionHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamPolicy)
|
|
||||||
private readonly policyRepo: Repository<IamPolicy>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -32,31 +28,9 @@ 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) {
|
||||||
|
|
||||||
const { identifier, accountId } = breakdownArn(PolicyArn);
|
|
||||||
const [_policy, name] = identifier.split('/');
|
|
||||||
const currentPolicy = await this.policyRepo.findOne({ where: { accountId, name, isDefault: true } });
|
|
||||||
|
|
||||||
if (SetAsDefault) {
|
|
||||||
await this.policyRepo.update({ accountId, name }, { isDefault: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
const policy = await this.policyRepo.create({
|
|
||||||
id: uuid.v4(),
|
|
||||||
name: name,
|
|
||||||
isDefault: SetAsDefault,
|
|
||||||
version: currentPolicy.version + 1,
|
|
||||||
document: PolicyDocument,
|
|
||||||
accountId: awsProperties.accountId,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
return {
|
|
||||||
PolicyVersion: {
|
|
||||||
IsDefaultVersion: policy.isDefault,
|
|
||||||
VersionId: `v${policy.version}`,
|
|
||||||
CreateDate: new Date(policy.createdAt).toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
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 { InjectRepository } from '@nestjs/typeorm';
|
import { IamService } from './iam.service';
|
||||||
import { Repository } from 'typeorm';
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
import * as uuid from 'uuid';
|
import { randomUUID } from 'crypto';
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
PolicyName: string;
|
Description: string;
|
||||||
|
Path: string;
|
||||||
PolicyDocument: string;
|
PolicyDocument: string;
|
||||||
}
|
PolicyName: string;
|
||||||
|
} & Record<string, string>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
|
export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamPolicy)
|
private readonly iamService: IamService,
|
||||||
private readonly policyRepo: Repository<IamPolicy>,
|
private readonly tagsService: TagsService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -25,30 +27,29 @@ export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
format = Format.Xml;
|
format = Format.Xml;
|
||||||
action = Action.IamCreatePolicy;
|
action = Action.IamCreatePolicy;
|
||||||
validator = Joi.object<QueryParams, true>({
|
validator = Joi.object<QueryParams, true>({
|
||||||
PolicyName: Joi.string().required(),
|
Description: Joi.string().max(1000).allow(null, '').default(null),
|
||||||
PolicyDocument: Joi.string().required(),
|
Path: Joi.string().min(1).max(512).default(null).regex(new RegExp(`((/[A-Za-z0-9\.,\+@=_-]+)*)/`)),
|
||||||
|
PolicyDocument: Joi.string().min(1).max(131072).required(),
|
||||||
|
PolicyName: Joi.string().min(1).max(128).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
protected async handle({ PolicyName, PolicyDocument }: QueryParams, awsProperties: AwsProperties) {
|
protected async handle(params: QueryParams, context: RequestContext) {
|
||||||
|
|
||||||
const policy = await this.policyRepo.create({
|
const { Description, Path, PolicyName, PolicyDocument } = params;
|
||||||
id: uuid.v4(),
|
|
||||||
|
const policy = await this.iamService.createPolicy({
|
||||||
|
id: randomUUID(),
|
||||||
|
version: 1,
|
||||||
|
isDefault: true,
|
||||||
name: PolicyName,
|
name: PolicyName,
|
||||||
document: PolicyDocument,
|
path: Path,
|
||||||
accountId: awsProperties.accountId,
|
description: Description,
|
||||||
}).save();
|
policy: PolicyDocument,
|
||||||
|
accountId: context.awsProperties.accountId,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Policy: {
|
Policy: policy.metadata
|
||||||
PolicyName: policy.name,
|
};
|
||||||
DefaultVersionId: policy.version,
|
|
||||||
PolicyId: policy.id,
|
|
||||||
Path: '/',
|
|
||||||
Arn: policy.arn,
|
|
||||||
AttachmentCount: 0,
|
|
||||||
CreateDate: new Date(policy.createdAt).toISOString(),
|
|
||||||
UpdateDate: new Date(policy.updatedAt).toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,23 @@ 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 { InjectRepository } from '@nestjs/typeorm';
|
import { IamService } from './iam.service';
|
||||||
import { Repository } from 'typeorm';
|
import { randomUUID } from 'crypto';
|
||||||
import { IamRole } from './iam-role.entity';
|
import { RequestContext } from '../_context/request.context';
|
||||||
import * as uuid from 'uuid';
|
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
RoleName: string;
|
RoleName: string;
|
||||||
Path: string;
|
Path: string;
|
||||||
AssumeRolePolicyDocument: string;
|
AssumeRolePolicyDocument: string;
|
||||||
MaxSessionDuration: number;
|
MaxSessionDuration: number;
|
||||||
|
Description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
|
export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamRole)
|
private readonly iamService: IamService,
|
||||||
private readonly roleRepo: Repository<IamRole>,
|
|
||||||
@InjectRepository(IamPolicy)
|
|
||||||
private readonly policyRepo: Repository<IamPolicy>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -30,33 +26,24 @@ export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
|
||||||
format = Format.Xml;
|
format = Format.Xml;
|
||||||
action = Action.IamCreateRole;
|
action = Action.IamCreateRole;
|
||||||
validator = Joi.object<QueryParams, true>({
|
validator = Joi.object<QueryParams, true>({
|
||||||
RoleName: Joi.string().required(),
|
AssumeRolePolicyDocument: Joi.string().min(1).max(131072).required(),
|
||||||
Path: Joi.string().required(),
|
Description: Joi.string().max(1000).allow(null, '').default(null),
|
||||||
AssumeRolePolicyDocument: Joi.string().required(),
|
MaxSessionDuration: Joi.number().min(3600).max(43200).default(3600),
|
||||||
MaxSessionDuration: Joi.number().default(3600),
|
Path: Joi.string().min(1).max(512).required(),
|
||||||
|
RoleName: Joi.string().min(1).max(64).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
protected async handle({ RoleName, Path, AssumeRolePolicyDocument, MaxSessionDuration }: QueryParams, awsProperties: AwsProperties) {
|
protected async handle({ RoleName, Path, AssumeRolePolicyDocument, MaxSessionDuration, Description }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
|
||||||
const policy = await this.policyRepo.create({
|
const role = await this.iamService.createRole({
|
||||||
id: uuid.v4(),
|
id: randomUUID(),
|
||||||
name: `${RoleName}-AssumeRolePolicyDocument`,
|
|
||||||
document: AssumeRolePolicyDocument,
|
|
||||||
accountId: awsProperties.accountId,
|
accountId: awsProperties.accountId,
|
||||||
}).save();
|
name: RoleName,
|
||||||
|
|
||||||
const id = uuid.v4();
|
|
||||||
|
|
||||||
await this.roleRepo.create({
|
|
||||||
id,
|
|
||||||
roleName: RoleName,
|
|
||||||
path: Path,
|
path: Path,
|
||||||
accountId: awsProperties.accountId,
|
assumeRolePolicy: AssumeRolePolicyDocument,
|
||||||
assumeRolePolicyDocumentId: policy.id,
|
|
||||||
maxSessionDuration: MaxSessionDuration,
|
maxSessionDuration: MaxSessionDuration,
|
||||||
}).save();
|
description: Description,
|
||||||
|
});
|
||||||
const role = await this.roleRepo.findOne({ where: { id }});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Role: role.metadata,
|
Role: role.metadata,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { IamService } from './iam.service';
|
||||||
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
RoleName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeleteRoleHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly iamService: IamService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Xml;
|
||||||
|
action = Action.IamDeleteRole;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
RoleName: Joi.string().min(1).max(64).required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ RoleName }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
await this.iamService.deleteRoleByName(awsProperties.accountId, RoleName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { Injectable, NotFoundException, Version } from '@nestjs/common';
|
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 { InjectRepository } from '@nestjs/typeorm';
|
import { RequestContext } from '../_context/request.context';
|
||||||
import { Repository } from 'typeorm';
|
import { IamService } from './iam.service';
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
import { breakdownArn } from '../util/breakdown-arn';
|
|
||||||
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
PolicyArn: string;
|
PolicyArn: string;
|
||||||
|
|
@ -17,10 +14,7 @@ type QueryParams = {
|
||||||
export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams> {
|
export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamPolicy)
|
private readonly iamService: IamService,
|
||||||
private readonly policyRepo: Repository<IamPolicy>,
|
|
||||||
@InjectRepository(IamRolePolicyAttachment)
|
|
||||||
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -32,22 +26,16 @@ 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 { identifier, accountId } = breakdownArn(PolicyArn);
|
const version = Number.isNaN(maybeVersion) ? Number(VersionId.toLowerCase().split('v')[1]) : Number(maybeVersion);
|
||||||
const [_policy, name] = identifier.split('/');
|
const policy = await this.iamService.getPolicyByArnAndVersion(PolicyArn, version);
|
||||||
const policy = await this.policyRepo.findOne({ where: { name, accountId, version: +VersionId }});
|
|
||||||
|
|
||||||
if (!policy) {
|
|
||||||
throw new NotFoundException('NoSuchEntity', 'The request was rejected because it referenced a resource entity that does not exist. The error message describes the resource.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
PolicyVersion: {
|
PolicyVersion: {
|
||||||
Document: policy.document,
|
Document: policy.policy,
|
||||||
IsDefaultVersion: policy.isDefault,
|
IsDefaultVersion: policy.isDefault,
|
||||||
VersionId: `${policy.version}`,
|
VersionId: policy.version,
|
||||||
CreateDate: new Date(policy.createdAt).toISOString(),
|
CreateDate: policy.createdAt.toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
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 { InjectRepository } from '@nestjs/typeorm';
|
import { RequestContext } from '../_context/request.context';
|
||||||
import { Repository } from 'typeorm';
|
import { IamService } from './iam.service';
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
import { breakdownArn } from '../util/breakdown-arn';
|
|
||||||
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
PolicyArn: string;
|
PolicyArn: string;
|
||||||
|
|
@ -16,10 +13,7 @@ type QueryParams = {
|
||||||
export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
|
export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamPolicy)
|
private readonly iamService: IamService,
|
||||||
private readonly policyRepo: Repository<IamPolicy>,
|
|
||||||
@InjectRepository(IamRolePolicyAttachment)
|
|
||||||
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -30,29 +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);
|
||||||
const { identifier, accountId } = breakdownArn(PolicyArn);
|
|
||||||
const [_policy, name] = identifier.split('/');
|
|
||||||
const policy = await this.policyRepo.findOne({ where: { name, accountId, isDefault: true }});
|
|
||||||
|
|
||||||
if (!policy) {
|
|
||||||
throw new NotFoundException('NoSuchEntity', 'The request was rejected because it referenced a resource entity that does not exist. The error message describes the resource.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachmentCount = await this.attachmentRepo.count({ where: { policyArn: policy.arn } });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Policy: {
|
Policy: policy.metadata,
|
||||||
PolicyName: policy.name,
|
|
||||||
DefaultVersionId: policy.version,
|
|
||||||
PolicyId: policy.id,
|
|
||||||
Path: '/',
|
|
||||||
Arn: policy.arn,
|
|
||||||
AttachmentCount: attachmentCount,
|
|
||||||
CreateDate: new Date(policy.createdAt).toISOString(),
|
|
||||||
UpdateDate: new Date(policy.updatedAt).toISOString(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
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 { InjectRepository } from '@nestjs/typeorm';
|
import { IamService } from './iam.service';
|
||||||
import { Repository } from 'typeorm';
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
import { IamRole } from './iam-role.entity';
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
RoleName: string;
|
RoleName: string;
|
||||||
|
|
@ -14,8 +14,7 @@ type QueryParams = {
|
||||||
export class GetRoleHandler extends AbstractActionHandler<QueryParams> {
|
export class GetRoleHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamRole)
|
private readonly iamService: IamService,
|
||||||
private readonly roleRepo: Repository<IamRole>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -23,17 +22,11 @@ export class GetRoleHandler extends AbstractActionHandler<QueryParams> {
|
||||||
format = Format.Xml;
|
format = Format.Xml;
|
||||||
action = Action.IamGetRole;
|
action = Action.IamGetRole;
|
||||||
validator = Joi.object<QueryParams, true>({
|
validator = Joi.object<QueryParams, true>({
|
||||||
RoleName: Joi.string().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.roleRepo.findOne({ where: { roleName: RoleName, accountId: awsProperties.accountId } });
|
|
||||||
|
|
||||||
if (!role) {
|
|
||||||
throw new NotFoundException('NoSuchEntity', 'The request was rejected because it referenced a resource entity that does not exist. The error message describes the resource.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Role: role.metadata,
|
Role: role.metadata,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,54 @@
|
||||||
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, OneToMany, OneToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
import { IamPolicy as PrismaIamPolicy } from "@prisma/client";
|
||||||
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
|
|
||||||
import { IamRole } from './iam-role.entity';
|
|
||||||
|
|
||||||
@Entity({ name: 'iam_policy' })
|
export class IamPolicy implements PrismaIamPolicy {
|
||||||
export class IamPolicy extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn()
|
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Column({ default: 1 })
|
|
||||||
version: number;
|
|
||||||
|
|
||||||
@Column({ name: 'is_default', default: true })
|
|
||||||
isDefault: boolean;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
document: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', nullable: false })
|
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
name: string;
|
||||||
|
version: number;
|
||||||
|
isDefault: boolean;
|
||||||
|
policy: string;
|
||||||
|
path: string | null;
|
||||||
|
description: string | null;
|
||||||
|
isAttachable: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
@CreateDateColumn()
|
constructor(p: PrismaIamPolicy) {
|
||||||
createdAt: string;
|
this.id = p.id;
|
||||||
|
this.accountId = p.accountId;
|
||||||
@UpdateDateColumn()
|
this.name = p.name;
|
||||||
updatedAt: string;
|
this.version = p.version;
|
||||||
|
this.isDefault = p.isDefault;
|
||||||
@OneToOne(() => IamRole, role => role.assumeRolePolicyDocument)
|
this.policy = p.policy;
|
||||||
iamRole: IamRole;
|
this.path = p.path;
|
||||||
|
this.description = p.description;
|
||||||
|
this.isAttachable = p.isAttachable;
|
||||||
|
this.createdAt = p.createdAt;
|
||||||
|
this.updatedAt = p.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
get arn() {
|
get arn() {
|
||||||
return `arn:aws:iam::${this.accountId}:policy/${this.name}`;
|
const parts = ['policy'];
|
||||||
|
if (this.path && this.path !== '/') {
|
||||||
|
parts.push(this.path);
|
||||||
|
}
|
||||||
|
parts.push(this.name);
|
||||||
|
return `arn:aws:iam::${this.accountId}:${parts.join('/')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get metadata() {
|
||||||
|
return {
|
||||||
|
Arn: this.arn,
|
||||||
|
AttachmentCount: 0,
|
||||||
|
CreateDate: this.createdAt.toISOString(),
|
||||||
|
DefaultVersionId: `v${this.version}`,
|
||||||
|
Description: this.description,
|
||||||
|
IsAttachable: this.isAttachable,
|
||||||
|
Path: this.path,
|
||||||
|
PolicyId: this.id,
|
||||||
|
PolicyName: this.name,
|
||||||
|
UpdateDate: this.updatedAt.toISOString(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
|
|
||||||
@Entity({ name: 'iam_role_policy_attachment' })
|
|
||||||
export class IamRolePolicyAttachment extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn()
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'policy_arn' })
|
|
||||||
policyArn: string;
|
|
||||||
|
|
||||||
@Column({ name: 'role_name' })
|
|
||||||
roleId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id'})
|
|
||||||
accountId: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +1,50 @@
|
||||||
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
|
|
||||||
@Entity({ name: 'iam_role' })
|
import { IamRole as PrismaIamRole } from '@prisma/client';
|
||||||
export class IamRole extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn()
|
export class IamRole implements PrismaIamRole {
|
||||||
id: string
|
|
||||||
|
|
||||||
@Column({ name: 'role_name' })
|
|
||||||
roleName: string;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
path: string;
|
|
||||||
|
|
||||||
@Column({ name: 'assume_role_policy_document_id', nullable: false })
|
|
||||||
assumeRolePolicyDocumentId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', nullable: false })
|
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
path: string | null;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
id: string;
|
||||||
|
maxSessionDuration: number | null;
|
||||||
|
assumeRolePolicy: string | null;
|
||||||
|
description: string | null;
|
||||||
|
permissionBoundaryArn: string | null;
|
||||||
|
lastUsedDate: Date | null;
|
||||||
|
lastUsedRegion: string | null;
|
||||||
|
|
||||||
@Column({ name: 'max_session_duration', nullable: false, default: 0 })
|
constructor(p: PrismaIamRole) {
|
||||||
maxSessionDuration: number;
|
this.accountId = p.accountId;
|
||||||
|
this.path = p.path;
|
||||||
@CreateDateColumn()
|
this.name = p.name;
|
||||||
createdAt: string;
|
this.createdAt = p.createdAt;
|
||||||
|
this.id = p.id;
|
||||||
@UpdateDateColumn()
|
this.maxSessionDuration = p.maxSessionDuration;
|
||||||
updatedAt: string;
|
this.assumeRolePolicy = p.assumeRolePolicy;
|
||||||
|
this.description = p.description;
|
||||||
@OneToOne(() => IamPolicy, (policy) => policy.id, { eager: true })
|
this.permissionBoundaryArn = p.permissionBoundaryArn;
|
||||||
@JoinColumn({ name: 'assume_role_policy_document_id' })
|
this.lastUsedDate = p.lastUsedDate;
|
||||||
assumeRolePolicyDocument: IamPolicy;
|
this.lastUsedRegion = p.lastUsedRegion;
|
||||||
|
}
|
||||||
|
|
||||||
get arn() {
|
get arn() {
|
||||||
const identifier = this.path.split('/');
|
const parts = ['role'];
|
||||||
identifier.push(this.roleName);
|
if (this.path && this.path !== '/') {
|
||||||
return `arn:aws:iam::${this.accountId}:role/${identifier.join('/')}`;
|
parts.push(this.path);
|
||||||
|
}
|
||||||
|
parts.push(this.name);
|
||||||
|
return `arn:aws:iam::${this.accountId}:${parts.join('/')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get metadata() {
|
get metadata() {
|
||||||
return {
|
return {
|
||||||
Path: this.path,
|
Path: this.path,
|
||||||
Arn: this.arn,
|
Arn: this.arn,
|
||||||
RoleName: this.roleName,
|
RoleName: this.name,
|
||||||
AssumeRolePolicyDocument: this.assumeRolePolicyDocument.document,
|
AssumeRolePolicyDocument: this.assumeRolePolicy,
|
||||||
CreateDate: new Date(this.createdAt).toISOString(),
|
CreateDate: this.createdAt.toISOString(),
|
||||||
RoleId: this.id,
|
RoleId: this.id,
|
||||||
MaxSessionDuration: this.maxSessionDuration,
|
MaxSessionDuration: this.maxSessionDuration,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,28 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Format } from '../abstract-action.handler';
|
import { Format } from '../abstract-action.handler';
|
||||||
import { Action } from '../action.enum';
|
import { Action } from '../action.enum';
|
||||||
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||||
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||||
import { AttachRolePolicyHandler } from './attach-role-policy.handler';
|
|
||||||
import { CreatePolicyVersionHandler } from './create-policy-version.handler';
|
|
||||||
import { CreatePolicyHandler } from './create-policy.handler';
|
import { CreatePolicyHandler } from './create-policy.handler';
|
||||||
import { CreateRoleHandler } from './create-role.handler';
|
import { CreateRoleHandler } from './create-role.handler';
|
||||||
import { GetPolicyVersionHandler } from './get-policy-version.handler';
|
|
||||||
import { GetPolicyHandler } from './get-policy.handler';
|
|
||||||
import { GetRoleHandler } from './get-role.handler';
|
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
|
|
||||||
import { IamRole } from './iam-role.entity';
|
|
||||||
import { IAMHandlers } from './iam.constants';
|
import { IAMHandlers } from './iam.constants';
|
||||||
|
import { PrismaModule } from '../_prisma/prisma.module';
|
||||||
|
import { IamService } from './iam.service';
|
||||||
|
import { GetRoleHandler } from './get-role.handler';
|
||||||
|
import { GetPolicyHandler } from './get-policy.handler';
|
||||||
|
import { GetPolicyVersionHandler } from './get-policy-version.handler';
|
||||||
|
import { AttachRolePolicyHandler } from './attach-role-policy.handler';
|
||||||
import { ListAttachedRolePoliciesHandler } from './list-attached-role-policies';
|
import { ListAttachedRolePoliciesHandler } from './list-attached-role-policies';
|
||||||
import { ListRolePoliciesHandler } from './list-role-policies.handler';
|
|
||||||
|
|
||||||
const handlers = [
|
const handlers = [
|
||||||
AttachRolePolicyHandler,
|
AttachRolePolicyHandler,
|
||||||
CreatePolicyHandler,
|
CreatePolicyHandler,
|
||||||
CreatePolicyVersionHandler,
|
|
||||||
CreateRoleHandler,
|
CreateRoleHandler,
|
||||||
|
GetPolicyVersionHandler,
|
||||||
GetPolicyHandler,
|
GetPolicyHandler,
|
||||||
GetRoleHandler,
|
GetRoleHandler,
|
||||||
GetPolicyVersionHandler,
|
|
||||||
ListAttachedRolePoliciesHandler,
|
ListAttachedRolePoliciesHandler,
|
||||||
ListRolePoliciesHandler,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
|
|
@ -194,11 +188,12 @@ const actions = [
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([IamPolicy, IamRole, IamRolePolicyAttachment]),
|
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
|
PrismaModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
...handlers,
|
...handlers,
|
||||||
|
IamService,
|
||||||
ExistingActionHandlersProvider(handlers),
|
ExistingActionHandlersProvider(handlers),
|
||||||
DefaultActionHandlerProvider(IAMHandlers, Format.Xml, actions),
|
DefaultActionHandlerProvider(IAMHandlers, Format.Xml, actions),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { PrismaService } from "../_prisma/prisma.service";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { IamPolicy } from "./iam-policy.entity";
|
||||||
|
import { IamRole } from "./iam-role.entity";
|
||||||
|
import { EntityAlreadyExists, NoSuchEntity, NotFoundException } from "../aws-shared-entities/aws-exceptions";
|
||||||
|
import { ArnUtil } from "../util/arn-util.static";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IamService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createRole(data: Prisma.IamRoleCreateInput): Promise<IamRole> {
|
||||||
|
try {
|
||||||
|
const record = await this.prismaService.iamRole.create({ data });
|
||||||
|
return new IamRole(record);
|
||||||
|
} catch (err) {
|
||||||
|
throw new EntityAlreadyExists(`RoleName ${data.name} already exists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneRoleByName(accountId: string, name: string): Promise<IamRole> {
|
||||||
|
try {
|
||||||
|
const record = await this.prismaService.iamRole.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
name,
|
||||||
|
accountId,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new IamRole(record);
|
||||||
|
} catch (error) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRoleByName(accountId: string, name: string) {
|
||||||
|
await this.prismaService.iamRole.deleteMany({
|
||||||
|
where: {
|
||||||
|
name,
|
||||||
|
accountId,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listRolePolicies(): Promise<IamPolicy[]> {
|
||||||
|
// return await this.prismaService;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPolicyByArn(arn: string): Promise<IamPolicy> {
|
||||||
|
try {
|
||||||
|
const name = arn.split('/')[1];
|
||||||
|
const record = await this.prismaService.iamPolicy.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
version: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return new IamPolicy(record);
|
||||||
|
} catch (err) {
|
||||||
|
throw new NoSuchEntity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPolicyByArnAndVersion(arn: string, version: number): Promise<IamPolicy> {
|
||||||
|
try {
|
||||||
|
const name = arn.split('/')[1];
|
||||||
|
const record = await this.prismaService.iamPolicy.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new IamPolicy(record);
|
||||||
|
} catch (err) {
|
||||||
|
throw new NoSuchEntity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPolicy(data: Prisma.IamPolicyCreateInput): Promise<IamPolicy> {
|
||||||
|
try {
|
||||||
|
const record = await this.prismaService.iamPolicy.create({ data });
|
||||||
|
return new IamPolicy(record);
|
||||||
|
} catch (err) {
|
||||||
|
throw new EntityAlreadyExists(`PolicyName ${data.name} already exists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async attachPolicyToRoleName(accountId: string, arn: string, roleName: string) {
|
||||||
|
const policy = await this.getPolicyByArn(arn);
|
||||||
|
const role = await this.findOneRoleByName(accountId, roleName);
|
||||||
|
await this.prismaService.iamRoleIamPolicyAttachment.create({
|
||||||
|
data: {
|
||||||
|
iamPolicyId: policy.id,
|
||||||
|
iamRoleId: role.id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAttachedRolePoliciesByRoleName(accountId: string, roleName: string): Promise<IamPolicy[]> {
|
||||||
|
try {
|
||||||
|
const record = await this.prismaService.iamRole.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
name: roleName,
|
||||||
|
accountId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
policies: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const policyIds = record.policies.map(p => p.iamPolicyId);
|
||||||
|
const policies = await this.prismaService.iamPolicy.findMany({ where: {
|
||||||
|
id: {
|
||||||
|
in: policyIds,
|
||||||
|
},
|
||||||
|
isDefault: true,
|
||||||
|
}});
|
||||||
|
return policies.map(p => new IamPolicy(p));
|
||||||
|
} catch (error) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
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 { InjectRepository } from '@nestjs/typeorm';
|
import { RequestContext } from '../_context/request.context';
|
||||||
import { In, Repository } from 'typeorm';
|
import { IamService } from './iam.service';
|
||||||
import { IamRole } from './iam-role.entity';
|
|
||||||
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
|
|
||||||
import { IamPolicy } from './iam-policy.entity';
|
|
||||||
import { breakdownArn } from '../util/breakdown-arn';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
RoleName: string;
|
RoleName: string;
|
||||||
|
|
@ -17,12 +13,7 @@ type QueryParams = {
|
||||||
export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
|
export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamRole)
|
private readonly iamService: IamService,
|
||||||
private readonly roleRepo: Repository<IamRole>,
|
|
||||||
@InjectRepository(IamPolicy)
|
|
||||||
private readonly policyRepo: Repository<IamPolicy>,
|
|
||||||
@InjectRepository(IamRolePolicyAttachment)
|
|
||||||
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -33,25 +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);
|
||||||
const role = await this.roleRepo.findOne({ where: { roleName: RoleName, accountId: awsProperties.accountId } });
|
|
||||||
|
|
||||||
if (!role) {
|
|
||||||
throw new NotFoundException('NoSuchEntity', 'The request was rejected because it referenced a resource entity that does not exist. The error message describes the resource.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachments = await this.attachmentRepo.find({ where: { roleId: role.id } })
|
|
||||||
const policyIds = attachments.map(({ policyArn }) => breakdownArn(policyArn)).map(({ identifier }) => identifier.split('/')[1]);
|
|
||||||
const policies = await this.policyRepo.find({ where: { name: In(policyIds), isDefault: true } });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
AttachedPolicies: {
|
AttachedPolicies: policies.map(p => ({
|
||||||
member: [role.assumeRolePolicyDocument, ...policies].map(p => ({
|
member: {
|
||||||
PolicyName: p.name,
|
PolicyName: p.name,
|
||||||
PolicyArn: p.arn,
|
PolicyArn: p.arn,
|
||||||
|
}
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
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 { InjectRepository } from '@nestjs/typeorm';
|
import { RequestContext } from '../_context/request.context';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { IamRole } from './iam-role.entity';
|
|
||||||
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
|
Marker: string;
|
||||||
|
MaxItems: number;
|
||||||
RoleName: string;
|
RoleName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,10 +14,6 @@ type QueryParams = {
|
||||||
export class ListRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
|
export class ListRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(IamRole)
|
|
||||||
private readonly roleRepo: Repository<IamRole>,
|
|
||||||
@InjectRepository(IamRolePolicyAttachment)
|
|
||||||
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -26,19 +21,13 @@ export class ListRolePoliciesHandler extends AbstractActionHandler<QueryParams>
|
||||||
format = Format.Xml;
|
format = Format.Xml;
|
||||||
action = Action.IamListRolePolicies;
|
action = Action.IamListRolePolicies;
|
||||||
validator = Joi.object<QueryParams, true>({
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
Marker: Joi.string().allow(null),
|
||||||
|
MaxItems: Joi.number().min(1).max(1000).default(100),
|
||||||
RoleName: Joi.string().required(),
|
RoleName: Joi.string().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
|
protected async handle({ RoleName }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
|
||||||
const role = await this.roleRepo.findOne({ where: { roleName: RoleName, accountId: awsProperties.accountId } });
|
|
||||||
|
|
||||||
if (!role) {
|
|
||||||
throw new NotFoundException('NoSuchEntity', 'The request was rejected because it referenced a resource entity that does not exist. The error message describes the resource.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
PolicyNames: [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ 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 { KmsKeyAlias } from './kms-key-alias.entity';
|
import { KmsService } from './kms.service';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
import { Repository } from 'typeorm';
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
AliasName: string;
|
AliasName: string;
|
||||||
|
|
@ -15,8 +15,7 @@ type QueryParams = {
|
||||||
export class CreateAliasHandler extends AbstractActionHandler<QueryParams> {
|
export class CreateAliasHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(KmsKeyAlias)
|
private readonly kmsService: KmsService,
|
||||||
private readonly aliasRepo: Repository<KmsKeyAlias>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -24,17 +23,27 @@ export class CreateAliasHandler extends AbstractActionHandler<QueryParams> {
|
||||||
format = Format.Json;
|
format = Format.Json;
|
||||||
action = Action.KmsCreateAlias;
|
action = Action.KmsCreateAlias;
|
||||||
validator = Joi.object<QueryParams, true>({
|
validator = Joi.object<QueryParams, true>({
|
||||||
AliasName: Joi.string().required(),
|
|
||||||
TargetKeyId: Joi.string().required(),
|
TargetKeyId: Joi.string().required(),
|
||||||
|
AliasName: Joi.string().min(1).max(256).regex(new RegExp(`^alias/[a-zA-Z0-9/_-]+$`)).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
protected async handle({ AliasName, TargetKeyId }: QueryParams, awsProperties: AwsProperties) {
|
protected async handle({ TargetKeyId, AliasName }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
|
||||||
await this.aliasRepo.save({
|
const keyRecord = await this.kmsService.findOneByRef(TargetKeyId, awsProperties);
|
||||||
name: AliasName.split('/')[1],
|
|
||||||
targetKeyId: TargetKeyId,
|
if (!keyRecord) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.kmsService.createAlias({
|
||||||
accountId: awsProperties.accountId,
|
accountId: awsProperties.accountId,
|
||||||
region: awsProperties.region,
|
region: awsProperties.region,
|
||||||
|
name: AliasName,
|
||||||
|
kmsKey: {
|
||||||
|
connect: {
|
||||||
|
id: keyRecord.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { CustomerMasterKeySpec, KeySpec, KeyState, KeyUsageType, OriginType, Tag } from '@aws-sdk/client-kms';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { KmsService } from './kms.service';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import { keySpecToUsageType } from './kms-key.entity';
|
||||||
|
import { UnsupportedOperationException } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
|
type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> };
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
BypassPolicyLockoutSafetyCheck: boolean;
|
||||||
|
CustomerMasterKeySpec: CustomerMasterKeySpec;
|
||||||
|
CustomKeyStoreId: string;
|
||||||
|
Description: string;
|
||||||
|
KeySpec: KeySpec;
|
||||||
|
KeyUsage: KeyUsageType;
|
||||||
|
MultiRegion: boolean;
|
||||||
|
Origin: OriginType;
|
||||||
|
Policy: string;
|
||||||
|
Tags: NoUndefinedField<Tag>[];
|
||||||
|
XksKeyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateDefaultPolicy = (accountId: string) => JSON.stringify({
|
||||||
|
"Sid": "Enable IAM User Permissions",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {
|
||||||
|
"AWS": `arn:aws:iam::${accountId}:root`
|
||||||
|
},
|
||||||
|
"Action": "kms:*",
|
||||||
|
"Resource": "*"
|
||||||
|
})
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateKeyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly kmsService: KmsService,
|
||||||
|
private readonly tagsService: TagsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.KmsCreateKey;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
BypassPolicyLockoutSafetyCheck: Joi.boolean().default(false),
|
||||||
|
CustomerMasterKeySpec: Joi.string().allow(...Object.values(CustomerMasterKeySpec)),
|
||||||
|
CustomKeyStoreId: Joi.string().min(1).max(64),
|
||||||
|
Description: Joi.string().min(0).max(8192).default(''),
|
||||||
|
KeySpec: Joi.string().allow(...Object.values(KeySpec)).default(KeySpec.SYMMETRIC_DEFAULT),
|
||||||
|
KeyUsage: Joi.string().allow(...Object.values(KeyUsageType)).default(KeyUsageType.ENCRYPT_DECRYPT),
|
||||||
|
MultiRegion: Joi.boolean().default(false),
|
||||||
|
Origin: Joi.string().allow(...Object.values(OriginType)).default(OriginType.AWS_KMS),
|
||||||
|
Policy: Joi.string().min(1).max(32768),
|
||||||
|
Tags: Joi.array().items(
|
||||||
|
Joi.object<Tag, true>({
|
||||||
|
TagKey: Joi.string().min(1).max(128).required(),
|
||||||
|
TagValue: Joi.string().min(0).max(256).required(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
XksKeyId: Joi.when('Origin', {
|
||||||
|
is: OriginType.EXTERNAL_KEY_STORE,
|
||||||
|
then: Joi.string().min(1).max(128),
|
||||||
|
otherwise: Joi.forbidden(),
|
||||||
|
}) as unknown as Joi.StringSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ KeyUsage, Description, KeySpec, Origin, MultiRegion, Policy, Tags, CustomerMasterKeySpec }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
|
||||||
|
const keySpec = CustomerMasterKeySpec ?? KeySpec;
|
||||||
|
|
||||||
|
if (!keySpecToUsageType[keySpec].includes(KeyUsage)) {
|
||||||
|
throw new UnsupportedOperationException(`KeySpec ${KeySpec} is not valid for KeyUsage ${KeyUsage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = this.keyGeneratorMap[keySpec]();
|
||||||
|
|
||||||
|
const createdKey = await this.kmsService.createKmsKey({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
enabled: true,
|
||||||
|
usage: KeyUsage,
|
||||||
|
description: Description,
|
||||||
|
keySpec: keySpec,
|
||||||
|
keyState: KeyState.Enabled,
|
||||||
|
origin: Origin,
|
||||||
|
multiRegion: MultiRegion,
|
||||||
|
policy: Policy ?? generateDefaultPolicy(awsProperties.accountId),
|
||||||
|
key,
|
||||||
|
accountId: awsProperties.accountId,
|
||||||
|
region: awsProperties.region,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.tagsService.createMany(createdKey.arn, Tags.map(({ TagKey, TagValue }) => ({ key: TagKey, value: TagValue })));
|
||||||
|
|
||||||
|
return {
|
||||||
|
KeyMetadata: createdKey.metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private keyGeneratorMap: Record<KeySpec, () => Buffer> = {
|
||||||
|
ECC_NIST_P256: function (): Buffer {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'X9_62_prime256v1' });
|
||||||
|
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
|
||||||
|
},
|
||||||
|
ECC_NIST_P384: function (): Buffer {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'secp384r1' });
|
||||||
|
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
|
||||||
|
},
|
||||||
|
ECC_NIST_P521: function (): Buffer {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'secp521r1' });
|
||||||
|
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
|
||||||
|
},
|
||||||
|
ECC_SECG_P256K1: function (): Buffer {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' });
|
||||||
|
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
|
||||||
|
},
|
||||||
|
HMAC_224: function (): Buffer {
|
||||||
|
return crypto.randomBytes(32);
|
||||||
|
},
|
||||||
|
HMAC_256: function (): Buffer {
|
||||||
|
return crypto.randomBytes(32);
|
||||||
|
},
|
||||||
|
HMAC_384: function (): Buffer {
|
||||||
|
return crypto.randomBytes(32);
|
||||||
|
},
|
||||||
|
HMAC_512: function (): Buffer {
|
||||||
|
return crypto.randomBytes(32);
|
||||||
|
},
|
||||||
|
RSA_2048: function (): Buffer {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 2048,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'pkcs1',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs8',
|
||||||
|
format: 'pem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
|
||||||
|
},
|
||||||
|
RSA_3072: function (): Buffer {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 3072,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'pkcs1',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs8',
|
||||||
|
format: 'pem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
|
||||||
|
},
|
||||||
|
RSA_4096: function (): Buffer {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
||||||
|
modulusLength: 4096,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'pkcs1',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs8',
|
||||||
|
format: 'pem'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Buffer.from(JSON.stringify({ privateKey, publicKey }));
|
||||||
|
},
|
||||||
|
SM2: function (): Buffer {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
SYMMETRIC_DEFAULT: function (): Buffer {
|
||||||
|
return crypto.randomBytes(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,12 @@ 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 { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { KmsKey } from './kms-key.entity';
|
|
||||||
import { breakdownArn } from '../util/breakdown-arn';
|
|
||||||
import { KmsService } from './kms.service';
|
import { KmsService } from './kms.service';
|
||||||
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
|
GrantTokens?: string[];
|
||||||
KeyId: string;
|
KeyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,8 +16,6 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly kmsService: KmsService,
|
private readonly kmsService: KmsService,
|
||||||
@InjectRepository(KmsKey)
|
|
||||||
private readonly keyRepo: Repository<KmsKey>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -27,27 +24,16 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
action = Action.KmsDescribeKey;
|
action = Action.KmsDescribeKey;
|
||||||
validator = Joi.object<QueryParams, true>({
|
validator = Joi.object<QueryParams, true>({
|
||||||
KeyId: Joi.string().required(),
|
KeyId: Joi.string().required(),
|
||||||
|
GrantTokens: Joi.array().items(Joi.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
|
protected async handle({ KeyId }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
|
||||||
const searchable = KeyId.startsWith('arn') ? breakdownArn(KeyId) : {
|
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
|
||||||
service: 'kms',
|
|
||||||
region: awsProperties.region,
|
|
||||||
accountId: awsProperties.accountId,
|
|
||||||
identifier: KeyId,
|
|
||||||
};
|
|
||||||
const [ type, pk ] = searchable.identifier.split('/');
|
|
||||||
const keyId: Promise<string> = type === 'key' ?
|
|
||||||
Promise.resolve(pk) :
|
|
||||||
this.kmsService.findKeyIdFromAlias(pk, searchable);
|
|
||||||
|
|
||||||
|
if (!keyRecord) {
|
||||||
const keyRecord = await this.keyRepo.findOne({ where: {
|
throw new NotFoundException();
|
||||||
id: await keyId,
|
}
|
||||||
region: searchable.region,
|
|
||||||
accountId: searchable.accountId,
|
|
||||||
}});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
KeyMetadata: keyRecord.metadata,
|
KeyMetadata: keyRecord.metadata,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
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;
|
||||||
|
RotationPeriodInDays: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EnableKeyRotationHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly kmsService: KmsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.KmsEnableKeyRotation;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
KeyId: Joi.string().required(),
|
||||||
|
RotationPeriodInDays: Joi.number().min(90).max(2560).default(365),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ KeyId, RotationPeriodInDays }: QueryParams, context: RequestContext) {
|
||||||
|
|
||||||
|
const keyRecord = await this.kmsService.findOneByRef(KeyId, context.awsProperties);
|
||||||
|
|
||||||
|
if (!keyRecord) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = new Date();
|
||||||
|
next.setDate(next.getDate() + RotationPeriodInDays);
|
||||||
|
|
||||||
|
await this.kmsService.updateKmsKey(keyRecord.id, {
|
||||||
|
rotationPeriod: RotationPeriodInDays,
|
||||||
|
nextRotation: next,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
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 = {
|
||||||
|
PolicyName: string;
|
||||||
|
KeyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetKeyPolicyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly kmsService: KmsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.KmsGetKeyPolicy;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
KeyId: Joi.string().required(),
|
||||||
|
PolicyName: Joi.string().min(1).max(128).default('default'),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ KeyId, PolicyName }: QueryParams, context: RequestContext) {
|
||||||
|
|
||||||
|
const keyRecord = await this.kmsService.findOneByRef(KeyId, context.awsProperties);
|
||||||
|
|
||||||
|
if (!keyRecord) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
PolicyName,
|
||||||
|
Policy: keyRecord.policy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
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 GetKeyRotationStatusHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly kmsService: KmsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.KmsGetKeyRotationStatus;
|
||||||
|
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 {
|
||||||
|
KeyId: keyRecord.id,
|
||||||
|
KeyRotationEnabled: !!keyRecord.rotationPeriod,
|
||||||
|
NextRotationDate: keyRecord.nextRotation,
|
||||||
|
RotationPeriodInDays: keyRecord.rotationPeriod,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,45 +2,18 @@ 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 { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { KeySpec, KeyUsage, KmsKey } from './kms-key.entity';
|
|
||||||
import { breakdownArn } from '../util/breakdown-arn';
|
|
||||||
import { KmsService } from './kms.service';
|
import { KmsService } from './kms.service';
|
||||||
import * as crypto from 'crypto';
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
GrantTokens: string[];
|
|
||||||
KeyId: string;
|
KeyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StandardOutput {
|
|
||||||
KeyId: string;
|
|
||||||
KeySpec: KeySpec;
|
|
||||||
KeyUsage: KeyUsage;
|
|
||||||
PublicKey: string;
|
|
||||||
CustomerMasterKeySpec: KeySpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EncryptDecrypt extends StandardOutput {
|
|
||||||
KeyUsage: 'ENCRYPT_DECRYPT';
|
|
||||||
EncryptionAlgorithms: ('SYMMETRIC_DEFAULT' | 'RSAES_OAEP_SHA_1' | 'RSAES_OAEP_SHA_256' | 'SM2PKE')[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SignVerify extends StandardOutput {
|
|
||||||
KeyUsage: 'SIGN_VERIFY';
|
|
||||||
SigningAlgorithms: ('RSASSA_PSS_SHA_256' | 'RSASSA_PSS_SHA_384' | 'RSASSA_PSS_SHA_512' | 'RSASSA_PKCS1_V1_5_SHA_256' | 'RSASSA_PKCS1_V1_5_SHA_384' | 'RSASSA_PKCS1_V1_5_SHA_512' | 'ECDSA_SHA_256' | 'ECDSA_SHA_384' | 'ECDSA_SHA_512' | 'SM2DSA')[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type Output = EncryptDecrypt | SignVerify | StandardOutput;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetPublicKeyHandler extends AbstractActionHandler<QueryParams> {
|
export class GetPublicKeyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
||||||
@InjectRepository(KmsKey)
|
|
||||||
private readonly keyRepo: Repository<KmsKey>,
|
|
||||||
private readonly kmsService: KmsService,
|
private readonly kmsService: KmsService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -50,74 +23,19 @@ export class GetPublicKeyHandler extends AbstractActionHandler<QueryParams> {
|
||||||
action = Action.KmsGetPublicKey;
|
action = Action.KmsGetPublicKey;
|
||||||
validator = Joi.object<QueryParams, true>({
|
validator = Joi.object<QueryParams, true>({
|
||||||
KeyId: Joi.string().required(),
|
KeyId: Joi.string().required(),
|
||||||
GrantTokens: Joi.array().items(Joi.string()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties): Promise<Output> {
|
protected async handle({ KeyId }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
|
||||||
const searchable = KeyId.startsWith('arn') ? breakdownArn(KeyId) : {
|
const keyRecord = await this.kmsService.findOneByRef(KeyId, awsProperties);
|
||||||
service: 'kms',
|
|
||||||
region: awsProperties.region,
|
|
||||||
accountId: awsProperties.accountId,
|
|
||||||
identifier: KeyId,
|
|
||||||
};
|
|
||||||
const [ type, pk ] = searchable.identifier.split('/');
|
|
||||||
const keyId: Promise<string> = type === 'key' ?
|
|
||||||
Promise.resolve(pk) :
|
|
||||||
this.kmsService.findKeyIdFromAlias(pk, searchable);
|
|
||||||
|
|
||||||
|
if (!keyRecord) {
|
||||||
const keyRecord = await this.keyRepo.findOne({ where: {
|
throw new NotFoundException();
|
||||||
id: await keyId,
|
|
||||||
region: searchable.region,
|
|
||||||
accountId: searchable.accountId,
|
|
||||||
}});
|
|
||||||
|
|
||||||
const pubKeyObject = crypto.createPublicKey({
|
|
||||||
key: keyRecord.key,//.split(String.raw`\n`).join('\n'),
|
|
||||||
format: 'pem',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (keyRecord.usage === 'ENCRYPT_DECRYPT') {
|
|
||||||
return {
|
|
||||||
CustomerMasterKeySpec: keyRecord.keySpec,
|
|
||||||
EncryptionAlgorithms: [ "SYMMETRIC_DEFAULT" ],
|
|
||||||
KeyId: keyRecord.arn,
|
|
||||||
KeySpec: keyRecord.keySpec,
|
|
||||||
KeyUsage: keyRecord.usage,
|
|
||||||
PublicKey: Buffer.from(pubKeyObject.export({
|
|
||||||
format: 'der',
|
|
||||||
type: 'spki',
|
|
||||||
})).toString('base64'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyRecord.usage === 'SIGN_VERIFY') {
|
|
||||||
const PublicKey = Buffer.from(pubKeyObject.export({
|
|
||||||
format: 'der',
|
|
||||||
type: 'spki',
|
|
||||||
})).toString('base64')
|
|
||||||
|
|
||||||
console.log({PublicKey})
|
|
||||||
return {
|
|
||||||
CustomerMasterKeySpec: keyRecord.keySpec,
|
|
||||||
KeyId: keyRecord.arn,
|
|
||||||
KeySpec: keyRecord.keySpec,
|
|
||||||
KeyUsage: keyRecord.usage,
|
|
||||||
PublicKey,
|
|
||||||
SigningAlgorithms: [ 'RSASSA_PKCS1_V1_5_SHA_256' ]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
CustomerMasterKeySpec: keyRecord.keySpec,
|
...keyRecord.metadata,
|
||||||
KeyId: keyRecord.arn,
|
PublicKey: Buffer.from(keyRecord.keyPair.publicKey).toString('base64'),
|
||||||
KeySpec: keyRecord.keySpec,
|
|
||||||
KeyUsage: keyRecord.usage,
|
|
||||||
PublicKey: Buffer.from(pubKeyObject.export({
|
|
||||||
format: 'pem',
|
|
||||||
type: 'spki',
|
|
||||||
})).toString('utf-8'),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { KmsAlias as PrismaKeyAlias } from "@prisma/client"
|
||||||
|
|
||||||
|
export class KmsAlias implements PrismaKeyAlias {
|
||||||
|
|
||||||
|
name: string
|
||||||
|
accountId: string
|
||||||
|
region: string
|
||||||
|
kmsKeyId: string
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
constructor(p: PrismaKeyAlias) {
|
||||||
|
this.name = p.name;
|
||||||
|
this.accountId = p.accountId;
|
||||||
|
this.region = p.region;
|
||||||
|
this.kmsKeyId = p.kmsKeyId;
|
||||||
|
this.createdAt = p.createdAt;
|
||||||
|
this.updatedAt = p.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get arn() {
|
||||||
|
return `arn:aws:kms:${this.region}:${this.accountId}:${this.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toAws() {
|
||||||
|
return {
|
||||||
|
AliasArn: this.arn,
|
||||||
|
AliasName: this.name,
|
||||||
|
CreationDate: this.createdAt.getAwsTime(),
|
||||||
|
LastUpdatedDate: this.updatedAt.getAwsTime(),
|
||||||
|
TargetKeyId: this.kmsKeyId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity({ name: 'kms_key_alias' })
|
|
||||||
export class KmsKeyAlias extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ name: 'target_key_id' })
|
|
||||||
targetKeyId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', nullable: false })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'region', nullable: false })
|
|
||||||
region: string;
|
|
||||||
|
|
||||||
get arn() {
|
|
||||||
return `arn:aws:kms:${this.region}:${this.accountId}:alias/${this.name}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +1,121 @@
|
||||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm';
|
import { KeySpec, KeyUsageType, KeyState, AlgorithmSpec, OriginType, ExpirationModelType, KeyAgreementAlgorithmSpec, MacAlgorithmSpec, MultiRegionKeyType, SigningAlgorithmSpec } from '@aws-sdk/client-kms';
|
||||||
|
import { KmsKey as PrismaKmsKey } from '@prisma/client';
|
||||||
|
|
||||||
export type KeySpec = 'RSA_2048' | 'RSA_3072' | 'RSA_4096' | 'ECC_NIST_P256' | 'ECC_NIST_P384' | 'ECC_NIST_P521' | 'ECC_SECG_P256K1' | 'SYMMETRIC_DEFAULT' | 'HMAC_224' | 'HMAC_256' | 'HMAC_384' | 'HMAC_512' | 'SM2';
|
export const keySpecToUsageType: Record<KeySpec, KeyUsageType[]> = {
|
||||||
export type KeyUsage = 'SIGN_VERIFY' | 'ENCRYPT_DECRYPT' | 'GENERATE_VERIFY_MAC';
|
ECC_NIST_P256: [KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
|
||||||
|
ECC_NIST_P384: [KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
|
||||||
|
ECC_NIST_P521: [KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
|
||||||
|
ECC_SECG_P256K1: [KeyUsageType.SIGN_VERIFY],
|
||||||
|
HMAC_224: [KeyUsageType.GENERATE_VERIFY_MAC],
|
||||||
|
HMAC_256: [KeyUsageType.GENERATE_VERIFY_MAC],
|
||||||
|
HMAC_384: [KeyUsageType.GENERATE_VERIFY_MAC],
|
||||||
|
HMAC_512: [KeyUsageType.GENERATE_VERIFY_MAC],
|
||||||
|
RSA_2048: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY],
|
||||||
|
RSA_3072: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY],
|
||||||
|
RSA_4096: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY],
|
||||||
|
SM2: [KeyUsageType.ENCRYPT_DECRYPT, KeyUsageType.SIGN_VERIFY, KeyUsageType.KEY_AGREEMENT],
|
||||||
|
SYMMETRIC_DEFAULT: [KeyUsageType.ENCRYPT_DECRYPT]
|
||||||
|
}
|
||||||
|
|
||||||
@Entity({ name: 'kms_key'})
|
export class KmsKey implements PrismaKmsKey {
|
||||||
export class KmsKey extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn()
|
|
||||||
id: string;
|
id: string;
|
||||||
|
enabled: boolean;
|
||||||
@Column({ name: 'usage' })
|
usage: KeyUsageType;
|
||||||
usage: KeyUsage;
|
|
||||||
|
|
||||||
@Column({ name: 'description' })
|
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
@Column({ name: 'key_spec' })
|
|
||||||
keySpec: KeySpec;
|
keySpec: KeySpec;
|
||||||
|
keyState: KeyState;
|
||||||
@Column({ name: 'key' })
|
origin: OriginType;
|
||||||
key: string;
|
multiRegion: boolean;
|
||||||
|
policy: string;
|
||||||
@Column({ name: 'account_id', nullable: false })
|
key: Buffer;
|
||||||
|
nextRotation: Date | null;
|
||||||
|
rotationPeriod: number | null;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
||||||
@Column({ name: 'region', nullable: false })
|
|
||||||
region: string;
|
region: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
@CreateDateColumn()
|
constructor(p: PrismaKmsKey) {
|
||||||
createdAt: string;
|
this.id = p.id;
|
||||||
|
this.enabled = p.enabled;
|
||||||
|
this.usage = p.usage as KeyUsageType;
|
||||||
|
this.description = p.description;
|
||||||
|
this.keySpec = p.keySpec as KeySpec;
|
||||||
|
this.keyState = p.keyState as KeyState;
|
||||||
|
this.origin = p.origin as OriginType;
|
||||||
|
this.multiRegion = p.multiRegion;
|
||||||
|
this.policy = p.policy;
|
||||||
|
this.key = Buffer.from(p.key);
|
||||||
|
this.nextRotation = p.nextRotation;
|
||||||
|
this.rotationPeriod = p.rotationPeriod;
|
||||||
|
this.accountId = p.accountId;
|
||||||
|
this.region = p.region;
|
||||||
|
this.createdAt = p.createdAt;
|
||||||
|
this.updatedAt = p.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
get arn() {
|
get arn() {
|
||||||
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> = {};
|
||||||
|
|
||||||
|
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.ENCRYPT_DECRYPT)) {
|
||||||
|
dynamicContent.EncryptionAlgorithms = Object.values(AlgorithmSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.origin === OriginType.EXTERNAL) {
|
||||||
|
dynamicContent.ExpirationModel = ExpirationModelType.KEY_MATERIAL_DOES_NOT_EXPIRE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.KEY_AGREEMENT)) {
|
||||||
|
dynamicContent.KeyAgreementAlgorithms = Object.values(KeyAgreementAlgorithmSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.GENERATE_VERIFY_MAC)) {
|
||||||
|
dynamicContent.MacAlgorithms = Object.values(MacAlgorithmSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.multiRegion) {
|
||||||
|
dynamicContent.MultiRegionConfiguration = {
|
||||||
|
MultiRegionKeyType: MultiRegionKeyType.PRIMARY,
|
||||||
|
PrimaryKey: {
|
||||||
|
Arn: this.arn,
|
||||||
|
Region: this.region,
|
||||||
|
},
|
||||||
|
ReplicaKeys: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keySpecToUsageType[this.keySpec].includes(KeyUsageType.SIGN_VERIFY)) {
|
||||||
|
dynamicContent.SigningAlgorithms = Object.values(SigningAlgorithmSpec);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
AWSAccountId: this.accountId,
|
AWSAccountId: this.accountId,
|
||||||
KeyId: this.id,
|
|
||||||
Arn: this.arn,
|
Arn: this.arn,
|
||||||
CreationDate: new Date(this.createdAt).toISOString(),
|
CreationDate: this.createdAt.getAwsTime(),
|
||||||
Enabled: true,
|
|
||||||
Description: this.description,
|
|
||||||
KeyUsage: this.usage,
|
|
||||||
KeyState: 'Enabled',
|
|
||||||
KeyManager: "CUSTOMER",
|
|
||||||
CustomerMasterKeySpec: this.keySpec,
|
CustomerMasterKeySpec: this.keySpec,
|
||||||
|
Description: this.description,
|
||||||
|
Enabled: true,
|
||||||
|
KeyId: this.id,
|
||||||
|
KeyManager: undefined,
|
||||||
KeySpec: this.keySpec,
|
KeySpec: this.keySpec,
|
||||||
DeletionDate: null,
|
KeyState: this.keyState,
|
||||||
SigningAlgorithms: [
|
KeyUsage: this.usage,
|
||||||
"RSASSA_PSS_SHA_256",
|
MultiRegion: this.multiRegion,
|
||||||
"RSASSA_PSS_SHA_384",
|
Origin: this.origin,
|
||||||
"RSASSA_PSS_SHA_512",
|
PendingDeletionWindowInDays: undefined,
|
||||||
"RSASSA_PKCS1_V1_5_SHA_256",
|
ValidTo: undefined,
|
||||||
"RSASSA_PKCS1_V1_5_SHA_384",
|
XksKeyConfiguration: undefined,
|
||||||
"RSASSA_PKCS1_V1_5_SHA_512"
|
...dynamicContent,
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,35 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Format } from '../abstract-action.handler';
|
import { Format } from '../abstract-action.handler';
|
||||||
import { Action } from '../action.enum';
|
import { Action } from '../action.enum';
|
||||||
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||||
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||||
import { CreateAliasHandler } from './create-alias.handler';
|
|
||||||
import { DescribeKeyHandler } from './describe-key.handler';
|
|
||||||
import { KmsKeyAlias } from './kms-key-alias.entity';
|
|
||||||
import { KmsKey } from './kms-key.entity';
|
|
||||||
import { KMSHandlers } from './kms.constants';
|
|
||||||
import { KmsService } from './kms.service';
|
import { KmsService } from './kms.service';
|
||||||
|
import { KMSHandlers } from './kms.constants';
|
||||||
|
import { DescribeKeyHandler } from './describe-key.handler';
|
||||||
|
import { PrismaModule } from '../_prisma/prisma.module';
|
||||||
|
import { ListAliasesHandler } from './list-aliases.handler';
|
||||||
|
import { CreateKeyHandler } from './create-key.handler';
|
||||||
|
import { EnableKeyRotationHandler } from './enable-key-rotation.handler';
|
||||||
|
import { GetKeyRotationStatusHandler } from './get-key-rotation-status.handler';
|
||||||
|
import { GetKeyPolicyHandler } from './get-key-policy.handler';
|
||||||
|
import { ListResourceTagsHandler } from './list-resource-tags.handler';
|
||||||
|
import { CreateAliasHandler } from './create-alias.handler';
|
||||||
import { GetPublicKeyHandler } from './get-public-key.handler';
|
import { GetPublicKeyHandler } from './get-public-key.handler';
|
||||||
|
import { SignHandler } from './sign.handler';
|
||||||
|
|
||||||
const handlers = [
|
const handlers = [
|
||||||
CreateAliasHandler,
|
CreateAliasHandler,
|
||||||
|
CreateKeyHandler,
|
||||||
DescribeKeyHandler,
|
DescribeKeyHandler,
|
||||||
|
EnableKeyRotationHandler,
|
||||||
|
GetKeyPolicyHandler,
|
||||||
|
GetKeyRotationStatusHandler,
|
||||||
GetPublicKeyHandler,
|
GetPublicKeyHandler,
|
||||||
|
ListAliasesHandler,
|
||||||
|
ListResourceTagsHandler,
|
||||||
|
SignHandler,
|
||||||
]
|
]
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
|
|
@ -74,8 +87,8 @@ const actions = [
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([KmsKey, KmsKeyAlias]),
|
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
|
PrismaModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
...handlers,
|
...handlers,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,117 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ArnParts } from '../util/breakdown-arn';
|
import { Prisma } from '@prisma/client';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { KmsKeyAlias } from './kms-key-alias.entity';
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
import { Repository } from 'typeorm';
|
import { breakdownArn } from '../util/breakdown-arn';
|
||||||
|
import { KmsKey } from './kms-key.entity';
|
||||||
|
import { KmsAlias } from './kms-alias.entity';
|
||||||
|
import { AwsProperties } from '../abstract-action.handler';
|
||||||
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class KmsService {
|
export class KmsService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(KmsKeyAlias)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly aliasRepo: Repository<KmsKeyAlias>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findKeyIdFromAlias(alias: string, arn: ArnParts): Promise<string> {
|
async findOneByRef(ref: string, awsProperties: AwsProperties): Promise<KmsKey> {
|
||||||
const record = await this.aliasRepo.findOne({ where: {
|
if (ref.startsWith('arn')) {
|
||||||
name: alias,
|
return await this.findOneByArn(ref);
|
||||||
accountId: arn.accountId,
|
}
|
||||||
region: arn.region,
|
return await this.findOneById(awsProperties.accountId, awsProperties.region, ref);
|
||||||
}});
|
}
|
||||||
return record.targetKeyId;
|
|
||||||
|
async findOneByArn(arn: string): Promise<KmsKey> {
|
||||||
|
const parts = breakdownArn(arn);
|
||||||
|
return await this.findOneById(parts.accountId, parts.region, parts.identifier.split('/')[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneById(accountId: string, region: string, ref: string): Promise<KmsKey> {
|
||||||
|
|
||||||
|
const [alias, record] = await Promise.all([
|
||||||
|
this.prismaService.kmsAlias.findFirst({
|
||||||
|
include: {
|
||||||
|
kmsKey: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
accountId,
|
||||||
|
region,
|
||||||
|
name: ref,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.prismaService.kmsKey.findFirst({
|
||||||
|
where: {
|
||||||
|
accountId,
|
||||||
|
region,
|
||||||
|
id: ref,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!alias?.kmsKey && !record) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return record ? new KmsKey(record) : new KmsKey(alias!.kmsKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAndCountAliasesByKeyId(accountId: string, region: string, limit: number, kmsKeyId: string, marker = ''): Promise<KmsAlias[]> {
|
||||||
|
const take = limit + 1;
|
||||||
|
const records = await this.prismaService.kmsAlias.findMany({
|
||||||
|
where: {
|
||||||
|
accountId,
|
||||||
|
region,
|
||||||
|
kmsKeyId,
|
||||||
|
name: {
|
||||||
|
gte: marker,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
take,
|
||||||
|
orderBy: {
|
||||||
|
name: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map(r => new KmsAlias(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAndCountAliases(accountId: string, region: string, limit: number, marker = ''): Promise<KmsAlias[]> {
|
||||||
|
const take = limit + 1;
|
||||||
|
const records = await this.prismaService.kmsAlias.findMany({
|
||||||
|
where: {
|
||||||
|
accountId,
|
||||||
|
region,
|
||||||
|
name: {
|
||||||
|
gte: marker,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
take,
|
||||||
|
orderBy: {
|
||||||
|
name: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map(r => new KmsAlias(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createKmsKey(data: Prisma.KmsKeyCreateInput): Promise<KmsKey> {
|
||||||
|
const record = await this.prismaService.kmsKey.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
return new KmsKey(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateKmsKey(id: string, data: Prisma.KmsKeyUpdateInput): Promise<void> {
|
||||||
|
await this.prismaService.kmsKey.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAlias(data: Prisma.KmsAliasCreateInput) {
|
||||||
|
await this.prismaService.kmsAlias.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
|
import { KmsService } from './kms.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
KeyId?: string;
|
||||||
|
Limit: number;
|
||||||
|
Marker?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ListAliasesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly kmsService: KmsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.KmsListAliases;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
KeyId: Joi.string(),
|
||||||
|
Limit: Joi.number().min(1).max(100).default(50),
|
||||||
|
Marker: Joi.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ KeyId, Limit, Marker }: QueryParams, { awsProperties} : RequestContext) {
|
||||||
|
|
||||||
|
const records = await (KeyId
|
||||||
|
? this.kmsService.findAndCountAliasesByKeyId(awsProperties.accountId, awsProperties.region, Limit, KeyId, Marker)
|
||||||
|
: this.kmsService.findAndCountAliases(awsProperties.accountId, awsProperties.region, Limit, Marker)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextMarker = records.length > Limit ? records.pop() : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
Aliases: records.map(r => r.toAws()),
|
||||||
|
NextMarker: nextMarker?.name,
|
||||||
|
Truncated: !!nextMarker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
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 { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
|
type QueryParams = {
|
||||||
|
KeyId: string;
|
||||||
|
Limit: number;
|
||||||
|
Marker: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ListResourceTagsHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly kmsService: KmsService,
|
||||||
|
private readonly tagsService: TagsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
format = Format.Json;
|
||||||
|
action = Action.KmsListResourceTags;
|
||||||
|
validator = Joi.object<QueryParams, true>({
|
||||||
|
KeyId: Joi.string().required(),
|
||||||
|
Limit: Joi.number().min(1).max(100).default(50),
|
||||||
|
Marker: Joi.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected async handle({ KeyId }: QueryParams, context: RequestContext) {
|
||||||
|
|
||||||
|
const keyRecord = await this.kmsService.findOneByRef(KeyId, context.awsProperties);
|
||||||
|
|
||||||
|
if (!keyRecord) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = await this.tagsService.getByArn(keyRecord.arn);
|
||||||
|
|
||||||
|
return {
|
||||||
|
Tags: tags.map(({ name, value }) => ({ TagKey: name, TagValue: value })),
|
||||||
|
Truncated: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main.ts
27
src/main.ts
|
|
@ -1,19 +1,32 @@
|
||||||
import { ClassSerializerInterceptor } from '@nestjs/common';
|
import { ClassSerializerInterceptor } from '@nestjs/common';
|
||||||
import { NestFactory, Reflector } from '@nestjs/core';
|
|
||||||
import { AppModule } from './app.module';
|
|
||||||
import * as morgan from 'morgan';
|
|
||||||
import { CommonConfig } from './config/common-config.interface';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { NestFactory, Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { CommonConfig } from './config/common-config.interface';
|
||||||
|
import { AwsExceptionFilter } from './_context/exception.filter';
|
||||||
|
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Date {
|
||||||
|
getAwsTime(): number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Date.prototype.getAwsTime = function (this: Date) {
|
||||||
|
return Math.floor(this.getTime() / 1000);
|
||||||
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.use(morgan('dev'));
|
// app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
||||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
|
app.useGlobalFilters(new AwsExceptionFilter());
|
||||||
|
app.use(bodyParser.json({ type: 'application/x-amz-json-1.0'}));
|
||||||
app.use(bodyParser.json({ type: 'application/x-amz-json-1.1'}));
|
app.use(bodyParser.json({ type: 'application/x-amz-json-1.1'}));
|
||||||
|
|
||||||
const configService: ConfigService<CommonConfig> = app.get(ConfigService)
|
const configService: ConfigService<CommonConfig, true> = app.get(ConfigService);
|
||||||
|
|
||||||
await app.listen(configService.get('PORT'), () => console.log(`Listening on port ${configService.get('PORT')}`));
|
await app.listen(configService.get('PORT'), () => console.log(`Listening on port ${configService.get('PORT')}`));
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export interface CreateSecretDto {
|
|
||||||
versionId?: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
secretString?: string;
|
|
||||||
accountId: string;
|
|
||||||
region: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
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 * as Joi from 'joi';
|
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;
|
||||||
|
|
@ -25,23 +29,25 @@ 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;
|
||||||
|
|
||||||
const secret = await this.secretService.create({
|
const secret = await this.secretService.create({
|
||||||
versionId: ClientRequestToken,
|
versionId: ClientRequestToken ?? randomUUID(),
|
||||||
description,
|
description,
|
||||||
name,
|
name,
|
||||||
secretString,
|
secretString,
|
||||||
accountId: awsProperties.accountId,
|
accountId: context.awsProperties.accountId,
|
||||||
region: awsProperties.region,
|
region: context.awsProperties.region,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name };
|
const arn = ArnUtil.fromSecret(secret);
|
||||||
|
|
||||||
|
return { ARN: arn, VersionId: secret.versionId, Name: secret.name };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
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 * as Joi from 'joi';
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
import { Secret } from './secret.entity';
|
|
||||||
import { SecretService } from './secret.service';
|
import { SecretService } from './secret.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
SecretId: string;
|
SecretId: string;
|
||||||
|
|
@ -15,6 +18,7 @@ export class DeleteSecretHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly secretService: SecretService,
|
private readonly secretService: SecretService,
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -26,9 +30,9 @@ 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 = Secret.getNameFromSecretId(SecretId);
|
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
|
||||||
const secret = VersionId ?
|
const secret = VersionId ?
|
||||||
await this.secretService.findByNameAndVersion(name, VersionId) :
|
await this.secretService.findByNameAndVersion(name, VersionId) :
|
||||||
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
||||||
|
|
@ -37,10 +41,20 @@ export class DeleteSecretHandler extends AbstractActionHandler {
|
||||||
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.deletionDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 5).toISOString();
|
await this.prismaService.secret.update({
|
||||||
await secret.save();
|
data: {
|
||||||
|
deletionDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 5),
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
versionId: secret.versionId,
|
||||||
|
name: secret.name,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const arn = ArnUtil.fromSecret(secret);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Arn: secret.arn,
|
Arn: arn,
|
||||||
DeletionDate: secret.deletionDate,
|
DeletionDate: secret.deletionDate,
|
||||||
Name: secret.name,
|
Name: secret.name,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
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 { Secret } from './secret.entity';
|
|
||||||
import { TagsService } from '../aws-shared-entities/tags.service';
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
|
import { SecretService } from './secret.service';
|
||||||
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
SecretId: string;
|
SecretId: string;
|
||||||
|
|
@ -15,8 +17,7 @@ type QueryParams = {
|
||||||
export class DescribeSecretHandler extends AbstractActionHandler {
|
export class DescribeSecretHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Secret)
|
private readonly secretService: SecretService,
|
||||||
private readonly secretRepo: Repository<Secret>,
|
|
||||||
private readonly tagsService: TagsService,
|
private readonly tagsService: TagsService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -26,22 +27,21 @@ 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) {
|
||||||
|
|
||||||
console.log({ SecretId })
|
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
|
||||||
|
const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
||||||
const name = Secret.getNameFromSecretId(SecretId);
|
|
||||||
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await this.tagsService.getByArn(secret.arn);
|
const arn = ArnUtil.fromSecret(secret);
|
||||||
|
const tags = await this.tagsService.getByArn(arn);
|
||||||
const listOfTagPairs = TagsService.getJsonSafeTagsMap(tags);
|
const listOfTagPairs = TagsService.getJsonSafeTagsMap(tags);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ARN": secret.arn,
|
"ARN": arn,
|
||||||
"CreatedDate": new Date(secret.createdAt).toISOString(),
|
"CreatedDate": new Date(secret.createdAt).toISOString(),
|
||||||
"DeletedDate": secret.deletionDate ? new Date(secret.deletionDate).toISOString() : null,
|
"DeletedDate": secret.deletionDate ? new Date(secret.deletionDate).toISOString() : null,
|
||||||
"Description": secret.description,
|
"Description": secret.description,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
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 { Secret } from './secret.entity';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
|
import { SecretService } from './secret.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
SecretId: string;
|
SecretId: string;
|
||||||
|
|
@ -15,8 +16,7 @@ type QueryParams = {
|
||||||
export class GetResourcePolicyHandler extends AbstractActionHandler {
|
export class GetResourcePolicyHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Secret)
|
private readonly secretService: SecretService,
|
||||||
private readonly secretRepo: Repository<Secret>,
|
|
||||||
private readonly attributesService: AttributesService,
|
private readonly attributesService: AttributesService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -26,18 +26,19 @@ 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 = Secret.getNameFromSecretId(SecretId);
|
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
|
||||||
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
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 BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const attribute = await this.attributesService.getResourcePolicyByArn(secret.arn);
|
const arn = ArnUtil.fromSecret(secret);
|
||||||
|
const attribute = await this.attributesService.getResourcePolicyByArn(arn);
|
||||||
return {
|
return {
|
||||||
ARN: secret.arn,
|
ARN: arn,
|
||||||
Name: secret.name,
|
Name: secret.name,
|
||||||
ResourcePolicy: attribute?.value,
|
ResourcePolicy: attribute?.value,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
import { Secret } from './secret.entity';
|
|
||||||
import { SecretService } from './secret.service';
|
import { SecretService } from './secret.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
SecretId: string;
|
SecretId: string;
|
||||||
|
|
@ -26,9 +28,9 @@ 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 = Secret.getNameFromSecretId(SecretId);
|
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
|
||||||
const secret = VersionId ?
|
const secret = VersionId ?
|
||||||
await this.secretService.findByNameAndVersion(name, VersionId) :
|
await this.secretService.findByNameAndVersion(name, VersionId) :
|
||||||
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
|
||||||
|
|
@ -37,8 +39,10 @@ export class GetSecretValueHandler extends AbstractActionHandler {
|
||||||
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const arn = ArnUtil.fromSecret(secret);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ARN: secret.arn,
|
ARN: arn,
|
||||||
CreatedDate: new Date(secret.createdAt).valueOf() / 1000,
|
CreatedDate: new Date(secret.createdAt).valueOf() / 1000,
|
||||||
Name: secret.name,
|
Name: secret.name,
|
||||||
SecretString: secret.secretString,
|
SecretString: secret.secretString,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
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 { Secret } from './secret.entity';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
|
import { SecretService } from './secret.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
SecretId: string;
|
SecretId: string;
|
||||||
|
|
@ -16,8 +17,7 @@ type QueryParams = {
|
||||||
export class PutResourcePolicyHandler extends AbstractActionHandler {
|
export class PutResourcePolicyHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Secret)
|
private readonly secretService: SecretService,
|
||||||
private readonly secretRepo: Repository<Secret>,
|
|
||||||
private readonly attributesService: AttributesService,
|
private readonly attributesService: AttributesService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -30,18 +30,19 @@ 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 = Secret.getNameFromSecretId(SecretId);
|
const name = ArnUtil.getSecretNameFromSecretId(SecretId);
|
||||||
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
|
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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.attributesService.createResourcePolicy(secret.arn, ResourcePolicy);
|
const arn = ArnUtil.fromSecret(secret);
|
||||||
|
await this.attributesService.createResourcePolicy(arn, ResourcePolicy);
|
||||||
return {
|
return {
|
||||||
ARN: secret.arn,
|
ARN: arn,
|
||||||
Name: secret.name,
|
Name: secret.name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
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 * as Joi from 'joi';
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
import { Secret } from './secret.entity';
|
|
||||||
import { SecretService } from './secret.service';
|
import { SecretService } from './secret.service';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
ClientRequestToken?: string;
|
ClientRequestToken?: string;
|
||||||
|
|
@ -28,24 +31,26 @@ 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 = Secret.getNameFromSecretId(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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = await this.secretService.create({
|
const secret = await this.secretService.create({
|
||||||
versionId: ClientRequestToken,
|
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name, VersionStages: [] }
|
const arn = ArnUtil.fromSecret(secret);
|
||||||
|
|
||||||
|
return { ARN: arn, VersionId: secret.versionId, Name: secret.name, VersionStages: [] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { BaseEntity, Column, CreateDateColumn, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('secret')
|
|
||||||
export class Secret extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn({ name: 'versionId' })
|
|
||||||
versionId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'name', nullable: false })
|
|
||||||
@Index()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ name: 'description', nullable: true })
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@Column({ name: 'secret_string', nullable: true })
|
|
||||||
secretString: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', nullable: false })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'region', nullable: false })
|
|
||||||
region: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: string;
|
|
||||||
|
|
||||||
@Column({ name: 'deletion_date', nullable: true })
|
|
||||||
deletionDate: string;
|
|
||||||
|
|
||||||
get arn(): string {
|
|
||||||
return `arn:aws:secretsmanager:${this.region}:${this.accountId}:${this.name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getNameFromSecretId(secretId: string) {
|
|
||||||
const parts = secretId.split(':');
|
|
||||||
return parts.length > 1 ? parts.pop() : secretId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +1,30 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { Prisma, Secret } from '@prisma/client';
|
||||||
import { Repository } from 'typeorm';
|
import { randomUUID } from 'crypto';
|
||||||
import { CreateSecretDto } from './create-secret.dto';
|
|
||||||
import { Secret } from './secret.entity';
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
import * as uuid from 'uuid';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SecretService {
|
export class SecretService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Secret)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly secretRepo: Repository<Secret>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findLatestByNameAndRegion(name: string, region: string): Promise<Secret> {
|
async findLatestByNameAndRegion(name: string, region: string): Promise<Secret | null> {
|
||||||
return await this.secretRepo.findOne({ where: { name, region }, order: { createdAt: 'DESC' } });
|
return await this.prismaService.secret.findFirst({ where: { name, region }, orderBy: { createdAt: 'desc' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByNameAndVersion(name: string, versionId: string): Promise<Secret> {
|
async findByNameAndVersion(name: string, versionId: string): Promise<Secret | null> {
|
||||||
// TypeORM BUG: https://github.com/typeorm/typeorm/issues/5694 - Cannot use findOne here
|
return await this.prismaService.secret.findFirst({ where: { name, versionId } });
|
||||||
const [ secret ] = await this.secretRepo.find({ where: { name, versionId } });
|
|
||||||
return secret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateSecretDto): Promise<Secret> {
|
async create(data: Prisma.SecretCreateInput): Promise<Secret> {
|
||||||
return await this.secretRepo.create({
|
return await this.prismaService.secret.create({
|
||||||
...dto,
|
data: {
|
||||||
versionId: dto.versionId ?? uuid.v4(),
|
...data,
|
||||||
}).save();
|
versionId: data.versionId ?? randomUUID(),
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
|
import { PrismaModule } from '../_prisma/prisma.module';
|
||||||
import { Format } from '../abstract-action.handler';
|
import { Format } from '../abstract-action.handler';
|
||||||
import { Action } from '../action.enum';
|
import { Action } from '../action.enum';
|
||||||
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||||
|
|
@ -12,7 +13,6 @@ import { GetResourcePolicyHandler } from './get-resource-policy.handler';
|
||||||
import { GetSecretValueHandler } from './get-secret-value.handler';
|
import { GetSecretValueHandler } from './get-secret-value.handler';
|
||||||
import { PutResourcePolicyHandler } from './put-resource-policy.handler';
|
import { PutResourcePolicyHandler } from './put-resource-policy.handler';
|
||||||
import { PutSecretValueHandler } from './put-secret-value.handler';
|
import { PutSecretValueHandler } from './put-secret-value.handler';
|
||||||
import { Secret } from './secret.entity';
|
|
||||||
import { SecretService } from './secret.service';
|
import { SecretService } from './secret.service';
|
||||||
import { SecretsManagerHandlers } from './secrets-manager.constants';
|
import { SecretsManagerHandlers } from './secrets-manager.constants';
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ const actions = [
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Secret]),
|
PrismaModule,
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
import { CreateTopicCommand, CreateTopicCommandOutput, GetSubscriptionAttributesCommand, GetTopicAttributesCommand, ListTagsForResourceCommand, ListTopicsCommand, PublishCommand, SNSClient, SubscribeCommand, SubscribeCommandOutput } from '@aws-sdk/client-sns';
|
|
||||||
import { TestingModule } from '@nestjs/testing';
|
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { Tag } from '../../aws-shared-entities/tags.entity';
|
|
||||||
import { SnsTopicSubscription } from '../sns-topic-subscription.entity';
|
|
||||||
import { SnsTopic } from '../sns-topic.entity';
|
|
||||||
|
|
||||||
describe('SNS Module', () => {
|
|
||||||
|
|
||||||
let snsClient: SNSClient;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
snsClient = new SNSClient({
|
|
||||||
endpoint: globalThis.__ENDPOINT__,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const describeCleanup = async () => {
|
|
||||||
const testModule: TestingModule = globalThis.__TESTMODULE__;
|
|
||||||
|
|
||||||
const snsTopicRepo = testModule.get<Repository<SnsTopic>>(getRepositoryToken(SnsTopic));
|
|
||||||
await snsTopicRepo.delete({});
|
|
||||||
|
|
||||||
const subscriptionRepo = testModule.get<Repository<SnsTopicSubscription>>(getRepositoryToken(SnsTopicSubscription));
|
|
||||||
await subscriptionRepo.delete({});
|
|
||||||
|
|
||||||
const tagsRepo = testModule.get<Repository<Tag>>(getRepositoryToken(Tag));
|
|
||||||
await tagsRepo.delete({});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('creation', () => {
|
|
||||||
|
|
||||||
afterAll(describeCleanup);
|
|
||||||
|
|
||||||
it('can create a topic', async () => {
|
|
||||||
const response = await snsClient.send(new CreateTopicCommand({
|
|
||||||
Name: 'test-topic-1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(response.TopicArn).toBe('arn:aws:sns:us-east-1:000000000000:test-topic-1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can subscribe', async () => {
|
|
||||||
|
|
||||||
const topicResponse = await snsClient.send(new CreateTopicCommand({
|
|
||||||
Name: 'test-topic-2',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const response = await snsClient.send(new SubscribeCommand({
|
|
||||||
TopicArn: topicResponse.TopicArn,
|
|
||||||
Protocol: 'https',
|
|
||||||
Endpoint: 'https://google.com',
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(response.SubscriptionArn).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can publish', async () => {
|
|
||||||
|
|
||||||
const topicResponse = await snsClient.send(new CreateTopicCommand({
|
|
||||||
Name: 'test-topic-3',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const response = await snsClient.send(new PublishCommand({
|
|
||||||
Message: "hello world",
|
|
||||||
TopicArn: topicResponse.TopicArn,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(response.MessageId).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('reading', () => {
|
|
||||||
|
|
||||||
afterAll(describeCleanup);
|
|
||||||
|
|
||||||
let subscribedTopic: CreateTopicCommandOutput;
|
|
||||||
let subscription: SubscribeCommandOutput;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
subscribedTopic = await snsClient.send(new CreateTopicCommand({
|
|
||||||
Name: 'test-topic-4',
|
|
||||||
Tags: [{ Key: 'V_a', Value: 'a' }, { Key: 'V_b', Value: 'b', }]
|
|
||||||
}));
|
|
||||||
|
|
||||||
await snsClient.send(new CreateTopicCommand({
|
|
||||||
Name: 'test-topic-5',
|
|
||||||
}));
|
|
||||||
|
|
||||||
await snsClient.send(new CreateTopicCommand({
|
|
||||||
Name: 'test-topic-6',
|
|
||||||
}));
|
|
||||||
|
|
||||||
subscription = await snsClient.send(new SubscribeCommand({
|
|
||||||
TopicArn: subscribedTopic.TopicArn,
|
|
||||||
Protocol: 'https',
|
|
||||||
Endpoint: 'https://google.com',
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can get subscription attributes', async () => {
|
|
||||||
const response = await snsClient.send(new GetSubscriptionAttributesCommand({
|
|
||||||
SubscriptionArn: subscription.SubscriptionArn,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(response.Attributes).toEqual(expect.objectContaining({
|
|
||||||
"ConfirmationWasAuthenticated": "true",
|
|
||||||
"PendingConfirmation": "false",
|
|
||||||
"Owner": "000000000000",
|
|
||||||
"SubscriptionArn": subscription.SubscriptionArn,
|
|
||||||
"TopicArn": subscribedTopic.TopicArn,
|
|
||||||
"TracingConfig": "PassThrough"
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can get topic attributes', async () => {
|
|
||||||
|
|
||||||
const response = await snsClient.send(new GetTopicAttributesCommand({
|
|
||||||
TopicArn: subscribedTopic.TopicArn,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(response.Attributes).toEqual(expect.objectContaining({
|
|
||||||
"DisplayName": 'test-topic-4',
|
|
||||||
"Owner": "000000000000",
|
|
||||||
"SubscriptionsConfirmed": "1",
|
|
||||||
"SubscriptionsDeleted": "0",
|
|
||||||
"SubscriptionsPending": "0",
|
|
||||||
"TopicArn": subscribedTopic.TopicArn,
|
|
||||||
"TracingConfig": "PassThrough"
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can list tags for resource', async () => {
|
|
||||||
|
|
||||||
const response = await snsClient.send(new ListTagsForResourceCommand({
|
|
||||||
ResourceArn: subscribedTopic.TopicArn,
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(response.Tags).toHaveLength(2);
|
|
||||||
|
|
||||||
const map = new Map(response.Tags.map(({ Key, Value }) => [ Key, Value ]));
|
|
||||||
expect(map.get('V_a')).toBe('a');
|
|
||||||
expect(map.get('V_b')).toBe('b');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can list all topics', async () => {
|
|
||||||
const response = await snsClient.send(new ListTopicsCommand({}));
|
|
||||||
expect(response.Topics).toHaveLength(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// describe('updating', () => {
|
|
||||||
|
|
||||||
// afterAll(describeCleanup);
|
|
||||||
|
|
||||||
// });
|
|
||||||
|
|
||||||
// describe('deleting', () => {
|
|
||||||
|
|
||||||
// afterAll(describeCleanup);
|
|
||||||
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
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 { SnsTopic } from './sns-topic.entity';
|
|
||||||
import * as Joi from 'joi';
|
|
||||||
import { TagsService } from '../aws-shared-entities/tags.service';
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
Name: string;
|
Name: string;
|
||||||
|
|
@ -15,8 +16,7 @@ type QueryParams = {
|
||||||
export class CreateTopicHandler extends AbstractActionHandler<QueryParams> {
|
export class CreateTopicHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SnsTopic)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly snsTopicRepo: Repository<SnsTopic>,
|
|
||||||
private readonly tagsService: TagsService,
|
private readonly tagsService: TagsService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -26,19 +26,22 @@ 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.snsTopicRepo.create({
|
const topic = await this.prismaService.snsTopic.create({
|
||||||
|
data: {
|
||||||
name,
|
name,
|
||||||
accountId: awsProperties.accountId,
|
accountId: context.awsProperties.accountId,
|
||||||
region: awsProperties.region,
|
region: context.awsProperties.region,
|
||||||
}).save();
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const tags = TagsService.tagPairs(params);
|
const tags = TagsService.tagPairs(params);
|
||||||
await this.tagsService.createMany(topic.topicArn, tags);
|
const arn = ArnUtil.fromTopic(topic);
|
||||||
|
await this.tagsService.createMany(arn, tags);
|
||||||
|
|
||||||
return { TopicArn: topic.topicArn };
|
return { TopicArn: arn };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
SubscriptionArn: string;
|
SubscriptionArn: string;
|
||||||
|
|
@ -15,8 +16,7 @@ type QueryParams = {
|
||||||
export class GetSubscriptionAttributesHandler extends AbstractActionHandler {
|
export class GetSubscriptionAttributesHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SnsTopicSubscription)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly snsTopicSubscriptionRepo: Repository<SnsTopicSubscription>,
|
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -26,10 +26,10 @@ 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.snsTopicSubscriptionRepo.findOne({ where: { id }});
|
const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id }});
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -39,13 +39,13 @@ export class GetSubscriptionAttributesHandler extends AbstractActionHandler {
|
||||||
const attributeMap = attributes.reduce((m, a) => {
|
const attributeMap = attributes.reduce((m, a) => {
|
||||||
m[a.name] = a.value;
|
m[a.name] = a.value;
|
||||||
return m;
|
return m;
|
||||||
}, {});
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
ConfirmationWasAuthenticated: 'true',
|
ConfirmationWasAuthenticated: 'true',
|
||||||
PendingConfirmation: 'false',
|
PendingConfirmation: 'false',
|
||||||
Owner: subscription.accountId,
|
Owner: subscription.accountId,
|
||||||
SubscriptionArn: subscription.arn,
|
SubscriptionArn: ArnUtil.fromTopicSub(subscription),
|
||||||
TopicArn: subscription.topicArn,
|
TopicArn: subscription.topicArn,
|
||||||
...attributeMap,
|
...attributeMap,
|
||||||
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
|
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
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 { SnsTopic } from './sns-topic.entity';
|
|
||||||
import * as Joi from 'joi';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
TopicArn: string;
|
TopicArn: string;
|
||||||
|
|
@ -16,10 +16,7 @@ type QueryParams = {
|
||||||
export class GetTopicAttributesHandler extends AbstractActionHandler {
|
export class GetTopicAttributesHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SnsTopic)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly snsTopicRepo: Repository<SnsTopic>,
|
|
||||||
@InjectRepository(SnsTopicSubscription)
|
|
||||||
private readonly snsTopicSubscriptionRepo: Repository<SnsTopicSubscription>,
|
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -29,10 +26,10 @@ 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.snsTopicRepo.findOne({ where: { name }});
|
const topic = await this.prismaService.snsTopic.findFirst({ where: { name }});
|
||||||
|
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
|
|
@ -42,9 +39,9 @@ export class GetTopicAttributesHandler extends AbstractActionHandler {
|
||||||
const attributeMap = attributes.reduce((m, a) => {
|
const attributeMap = attributes.reduce((m, a) => {
|
||||||
m[a.name] = a.value;
|
m[a.name] = a.value;
|
||||||
return m;
|
return m;
|
||||||
}, {});
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
const subscriptionCount = await this.snsTopicSubscriptionRepo.count({ where: { topicArn: TopicArn } });
|
const subscriptionCount = await this.prismaService.snsTopicSubscription.count({ where: { topicArn: TopicArn } });
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
DisplayName: topic.name,
|
DisplayName: topic.name,
|
||||||
|
|
@ -52,7 +49,7 @@ export class GetTopicAttributesHandler extends AbstractActionHandler {
|
||||||
SubscriptionsConfirmed: `${subscriptionCount}`,
|
SubscriptionsConfirmed: `${subscriptionCount}`,
|
||||||
SubscriptionsDeleted: '0',
|
SubscriptionsDeleted: '0',
|
||||||
SubscriptionsPending: '0',
|
SubscriptionsPending: '0',
|
||||||
TopicArn: topic.topicArn,
|
TopicArn: ArnUtil.fromTopic(topic),
|
||||||
...attributeMap,
|
...attributeMap,
|
||||||
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
|
TracingConfig: attributeMap['TracingConfig'] ?? 'PassThrough',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
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 { SnsTopic } from './sns-topic.entity';
|
|
||||||
import * as Joi from 'joi';
|
|
||||||
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;
|
||||||
|
|
@ -24,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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
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 { SnsTopic } from './sns-topic.entity';
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
import * as Joi from 'joi';
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
NextToken: number;
|
NextToken: number;
|
||||||
|
|
@ -14,8 +15,7 @@ type QueryParams = {
|
||||||
export class ListTopicsHandler extends AbstractActionHandler {
|
export class ListTopicsHandler extends AbstractActionHandler {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SnsTopic)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly snsTopicRepo: Repository<SnsTopic>,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -24,10 +24,13 @@ 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 this.snsTopicRepo.findAndCount({ order: { name: 'DESC' }, take: 100, skip });
|
const [ topics, total ] = await Promise.all([
|
||||||
const response = { Topics: { member: topics.map(t => ({ TopicArn: t.topicArn } ))} };
|
this.prismaService.snsTopic.findMany({ orderBy: { name: 'desc' }, take: 100, skip }),
|
||||||
|
this.prismaService.snsTopic.count(),
|
||||||
|
]);
|
||||||
|
const response = { Topics: { member: topics.map(t => ({ TopicArn: ArnUtil.fromTopic(t) } ))} };
|
||||||
|
|
||||||
if (total >= 100) {
|
if (total >= 100) {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import * as Joi from 'joi';
|
||||||
import { Repository } from 'typeorm';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
import { SqsQueueEntryService } from '../sqs/sqs-queue-entry.service';
|
|
||||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
|
||||||
import * as uuid from 'uuid';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.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 { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
TopicArn: string;
|
TopicArn: string;
|
||||||
|
|
@ -21,8 +22,7 @@ type QueryParams = {
|
||||||
export class PublishHandler extends AbstractActionHandler<QueryParams> {
|
export class PublishHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SnsTopicSubscription)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly snsTopicSubscriptionRepo: Repository<SnsTopicSubscription>,
|
|
||||||
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
) {
|
) {
|
||||||
|
|
@ -38,21 +38,22 @@ 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) {
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageId = uuid.v4();
|
const MessageId = randomUUID();
|
||||||
const subscriptions = await this.snsTopicSubscriptionRepo.find({ where: { topicArn: arn } });
|
const subscriptions = await this.prismaService.snsTopicSubscription.findMany({ where: { topicArn: arn } });
|
||||||
const topicAttributes = await this.attributeService.getByArn(arn);
|
const topicAttributes = await this.attributeService.getByArn(arn);
|
||||||
|
|
||||||
for (const sub of subscriptions) {
|
for (const sub of subscriptions) {
|
||||||
const attributes = await this.attributeService.getByArn(sub.arn);
|
const subArn = ArnUtil.fromTopicSub(sub);
|
||||||
if (sub.protocol === 'sqs') {
|
const attributes = await this.attributeService.getByArn(subArn);
|
||||||
const { value: isRaw } = attributes.find(a => a.name === 'RawMessageDelivery');
|
if (sub.protocol === 'sqs' && sub.endpoint) {
|
||||||
|
const { value: isRaw } = attributes.find(a => a.name === 'RawMessageDelivery') ?? {};
|
||||||
const [queueAccountId, queueName] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(sub.endpoint);
|
const [queueAccountId, queueName] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(sub.endpoint);
|
||||||
|
|
||||||
const message = isRaw === 'true' ? Message : JSON.stringify({
|
const message = isRaw === 'true' ? Message : JSON.stringify({
|
||||||
|
|
@ -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=${sub.arn}`,
|
UnsubscribeURL: `${context.awsProperties.host}/?Action=Unsubscribe&SubscriptionArn=${subArn}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.sqsQueueEntryService.publish(queueAccountId, queueName, message);
|
await this.sqsQueueEntryService.publish(queueAccountId, queueName, message);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
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;
|
||||||
|
|
@ -27,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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
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;
|
||||||
|
|
@ -27,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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { BaseEntity, Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('sns_topic_subscription')
|
|
||||||
export class SnsTopicSubscription extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn({ name: 'id' })
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
@Column({ name: 'topic_arn' })
|
|
||||||
topicArn: string;
|
|
||||||
|
|
||||||
@Column({ name: 'endpoint', nullable: true })
|
|
||||||
endpoint: string;
|
|
||||||
|
|
||||||
@Column({ name: 'protocol' })
|
|
||||||
protocol: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', nullable: false })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'region', nullable: false })
|
|
||||||
region: string;
|
|
||||||
|
|
||||||
get arn() {
|
|
||||||
return `${this.topicArn}:${this.id}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
|
|
||||||
|
|
||||||
@Entity('sns_topic')
|
|
||||||
export class SnsTopic extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn({ name: 'name' })
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Column({ name: 'account_id', nullable: false })
|
|
||||||
accountId: string;
|
|
||||||
|
|
||||||
@Column({ name: 'region', nullable: false })
|
|
||||||
region: string;
|
|
||||||
|
|
||||||
get topicArn(): string {
|
|
||||||
return `arn:aws:sns:${this.region}:${this.accountId}:${this.name}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { Format } from '../abstract-action.handler';
|
||||||
import { AbstractActionHandler, Format } from '../abstract-action.handler';
|
|
||||||
import { Action } from '../action.enum';
|
import { Action } from '../action.enum';
|
||||||
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||||
import { ExistingActionHandlers } from '../default-action-handler/default-action-handler.constants';
|
|
||||||
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
|
||||||
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
|
||||||
import { SqsModule } from '../sqs/sqs.module';
|
import { SqsModule } from '../sqs/sqs.module';
|
||||||
|
|
@ -15,11 +13,10 @@ import { ListTopicsHandler } from './list-topics.handler';
|
||||||
import { PublishHandler } from './publish.handler';
|
import { PublishHandler } from './publish.handler';
|
||||||
import { SetSubscriptionAttributesHandler } from './set-subscription-attributes.handler';
|
import { SetSubscriptionAttributesHandler } from './set-subscription-attributes.handler';
|
||||||
import { SetTopicAttributesHandler } from './set-topic-attributes.handler';
|
import { SetTopicAttributesHandler } from './set-topic-attributes.handler';
|
||||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
|
||||||
import { SnsTopic } from './sns-topic.entity';
|
|
||||||
import { SnsHandlers } from './sns.constants';
|
import { SnsHandlers } from './sns.constants';
|
||||||
import { SubscribeHandler } from './subscribe.handler';
|
import { SubscribeHandler } from './subscribe.handler';
|
||||||
import { UnsubscribeHandler } from './unsubscribe.handler';
|
import { UnsubscribeHandler } from './unsubscribe.handler';
|
||||||
|
import { PrismaModule } from '../_prisma/prisma.module';
|
||||||
|
|
||||||
const handlers = [
|
const handlers = [
|
||||||
CreateTopicHandler,
|
CreateTopicHandler,
|
||||||
|
|
@ -81,8 +78,8 @@ const actions = [
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([SnsTopic, SnsTopicSubscription]),
|
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
|
PrismaModule,
|
||||||
SqsModule,
|
SqsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { randomUUID } from 'crypto';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
|
||||||
import { Action } from '../action.enum';
|
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import { TagsService } from '../aws-shared-entities/tags.service';
|
|
||||||
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
|
import { AbstractActionHandler, Format } from '../abstract-action.handler';
|
||||||
|
import { Action } from '../action.enum';
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
import * as uuid from 'uuid';
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
import { SqsQueueEntryService } from '../sqs/sqs-queue-entry.service';
|
import { RequestContext } from '../_context/request.context';
|
||||||
import { SqsQueue } from '../sqs/sqs-queue.entity';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
TopicArn: string;
|
TopicArn: string;
|
||||||
|
|
@ -21,11 +20,9 @@ type QueryParams = {
|
||||||
export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
|
export class SubscribeHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SnsTopicSubscription)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly snsTopicSubscription: Repository<SnsTopicSubscription>,
|
|
||||||
private readonly tagsService: TagsService,
|
private readonly tagsService: TagsService,
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -38,23 +35,27 @@ 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.snsTopicSubscription.create({
|
const subscription = await this.prismaService.snsTopicSubscription.create({
|
||||||
id: uuid.v4(),
|
data: {
|
||||||
|
id: randomUUID(),
|
||||||
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,
|
||||||
}).save();
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const arn = ArnUtil.fromTopicSub(subscription);
|
||||||
|
|
||||||
const tags = TagsService.tagPairs(params);
|
const tags = TagsService.tagPairs(params);
|
||||||
await this.tagsService.createMany(subscription.arn, tags);
|
await this.tagsService.createMany(arn, tags);
|
||||||
|
|
||||||
const attributes = AttributesService.attributePairs(params);
|
const attributes = AttributesService.attributePairs(params);
|
||||||
await this.attributeService.createMany(subscription.arn, attributes);
|
await this.attributeService.createMany(arn, attributes);
|
||||||
|
|
||||||
return { SubscriptionArn: subscription.arn };
|
return { SubscriptionArn: arn };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
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 { TagsService } from '../aws-shared-entities/tags.service';
|
import { TagsService } from '../aws-shared-entities/tags.service';
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
import { SnsTopicSubscription } from './sns-topic-subscription.entity';
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
import * as uuid from 'uuid';
|
import { ArnUtil } from '../util/arn-util.static';
|
||||||
|
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
SubscriptionArn: string;
|
SubscriptionArn: string;
|
||||||
|
|
@ -17,8 +16,7 @@ type QueryParams = {
|
||||||
export class UnsubscribeHandler extends AbstractActionHandler<QueryParams> {
|
export class UnsubscribeHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SnsTopicSubscription)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly snsTopicSubscription: Repository<SnsTopicSubscription>,
|
|
||||||
private readonly tagsService: TagsService,
|
private readonly tagsService: TagsService,
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
) {
|
) {
|
||||||
|
|
@ -31,13 +29,19 @@ 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.snsTopicSubscription.findOne({ where: { id } });
|
const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id } });
|
||||||
|
|
||||||
await this.tagsService.deleteByArn(subscription.arn);
|
if (!subscription) {
|
||||||
await this.attributeService.deleteByArn(subscription.arn);
|
throw new NotFoundException();
|
||||||
await this.snsTopicSubscription.delete({ id });
|
}
|
||||||
|
|
||||||
|
const arn = ArnUtil.fromTopicSub(subscription);
|
||||||
|
|
||||||
|
await this.tagsService.deleteByArn(arn);
|
||||||
|
await this.attributeService.deleteByArn(arn);
|
||||||
|
await this.prismaService.snsTopicSubscription.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,18 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
|
||||||
import { Action } from '../action.enum';
|
|
||||||
import * as Joi from 'joi';
|
import * as Joi from 'joi';
|
||||||
import { TagsService } from '../aws-shared-entities/tags.service';
|
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
import { Format } from '../abstract-action.handler';
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { Action } from '../action.enum';
|
||||||
|
import { V2CreateQueueHandler } from './v2-create-queue.handler';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
QueueName: string;
|
QueueName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateQueueHandler extends AbstractActionHandler<QueryParams> {
|
export class CreateQueueHandler extends V2CreateQueueHandler {
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(SqsQueue)
|
|
||||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
|
||||||
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.sqsQueueRepo.create({
|
|
||||||
name,
|
|
||||||
accountId: awsProperties.accountId,
|
|
||||||
region: awsProperties.region,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
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) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
|
||||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
QueueUrl: string;
|
QueueUrl: string;
|
||||||
}
|
} & Record<string, string>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams> {
|
export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
@ -25,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);
|
||||||
|
|
@ -33,7 +35,7 @@ export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams
|
||||||
for (const header of Object.keys(params)) {
|
for (const header of Object.keys(params)) {
|
||||||
if (header.includes('DeleteMessageBatchRequestEntry') && header.includes('ReceiptHandle')) {
|
if (header.includes('DeleteMessageBatchRequestEntry') && header.includes('ReceiptHandle')) {
|
||||||
const ReceiptHandle = params[header];
|
const ReceiptHandle = params[header];
|
||||||
await this.sqsQueueEntryService.deleteMessage(accountId, name, ReceiptHandle);
|
await this.sqsQueueEntryService.deleteMessage(ReceiptHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
|
||||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
QueueUrl: string;
|
QueueUrl: string;
|
||||||
|
|
@ -27,9 +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);
|
||||||
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
|
|
||||||
await this.sqsQueueEntryService.deleteMessage(accountId, name, ReceiptHandle);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
|
||||||
import { TagsService } from '../aws-shared-entities/tags.service';
|
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 = {
|
||||||
QueueUrl?: string,
|
QueueUrl?: string,
|
||||||
|
|
@ -18,8 +17,6 @@ type QueryParams = {
|
||||||
export class DeleteQueueHandler extends AbstractActionHandler<QueryParams> {
|
export class DeleteQueueHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SqsQueue)
|
|
||||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
|
||||||
private readonly tagsService: TagsService,
|
private readonly tagsService: TagsService,
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||||
|
|
@ -34,25 +31,18 @@ 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.sqsQueueRepo.findOne({ where: { accountId , name } });
|
const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name);
|
||||||
|
|
||||||
if(!queue) {
|
if (!queue) {
|
||||||
throw new BadRequestException('ResourceNotFoundException');
|
throw new BadRequestException('ResourceNotFoundException');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sqsQueueEntryService.purge(accountId, name);
|
await this.sqsQueueEntryService.purge(accountId, name);
|
||||||
await this.tagsService.deleteByArn(queue.arn);
|
await this.tagsService.deleteByArn(queue.arn);
|
||||||
await this.attributeService.deleteByArn(queue.arn);
|
await this.attributeService.deleteByArn(queue.arn);
|
||||||
await queue.remove();
|
await this.sqsQueueEntryService.deleteQueue(queue.id);
|
||||||
}
|
|
||||||
|
|
||||||
private async getAttributes(attributeNames: string[], queueArn: string) {
|
|
||||||
if (attributeNames.length === 0 || attributeNames.length === 1 && attributeNames[0] === 'All') {
|
|
||||||
return await this.attributeService.getByArn(queueArn);
|
|
||||||
}
|
|
||||||
return await this.attributeService.getByArnAndNames(queueArn, attributeNames);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,22 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
QueueUrl?: string,
|
QueueUrl?: string,
|
||||||
'AttributeName.1'?: string;
|
'AttributeName.1'?: string;
|
||||||
__path: string;
|
__path: string;
|
||||||
}
|
} & Record<string, string>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GetQueueAttributesHandler extends AbstractActionHandler<QueryParams> {
|
export class GetQueueAttributesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SqsQueue)
|
|
||||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||||
) {
|
) {
|
||||||
|
|
@ -34,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('.');
|
||||||
|
|
@ -42,23 +39,23 @@ export class GetQueueAttributesHandler extends AbstractActionHandler<QueryParams
|
||||||
l.push(params[k]);
|
l.push(params[k]);
|
||||||
}
|
}
|
||||||
return l;
|
return l;
|
||||||
}, []);
|
}, [] as string[]);
|
||||||
|
|
||||||
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(params.QueueUrl ?? params.__path);
|
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(params.QueueUrl ?? params.__path);
|
||||||
const queue = await this.sqsQueueRepo.findOne({ where: { accountId , name } });
|
const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name);
|
||||||
|
|
||||||
if(!queue) {
|
if(!queue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queueMetrics = this.sqsQueueEntryService.metrics(queue.arn);
|
const queueMetrics = await this.sqsQueueEntryService.metrics(queue.id);
|
||||||
const attributes = await this.getAttributes(attributeNames, queue.arn);
|
const attributes = await this.getAttributes(attributeNames, queue.arn);
|
||||||
const attributeMap = attributes.reduce((m, a) => {
|
const attributeMap = attributes.reduce((m, a) => {
|
||||||
m[a.name] = a.value;
|
m[a.name] = a.value;
|
||||||
return m;
|
return m;
|
||||||
}, {});
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
const response = {
|
const response: Record<string, string> = {
|
||||||
...attributeMap,
|
...attributeMap,
|
||||||
ApproximateNumberOfMessages: `${queueMetrics.total}`,
|
ApproximateNumberOfMessages: `${queueMetrics.total}`,
|
||||||
ApproximateNumberOfMessagesNotVisible: `${queueMetrics.inFlight}`,
|
ApproximateNumberOfMessagesNotVisible: `${queueMetrics.inFlight}`,
|
||||||
|
|
@ -66,7 +63,8 @@ export class GetQueueAttributesHandler extends AbstractActionHandler<QueryParams
|
||||||
LastModifiedTimestamp: `${new Date(queue.updatedAt).getTime()}`,
|
LastModifiedTimestamp: `${new Date(queue.updatedAt).getTime()}`,
|
||||||
QueueArn: queue.arn,
|
QueueArn: queue.arn,
|
||||||
}
|
}
|
||||||
return { Attribute: Object.keys(response).map(k => ({
|
return {
|
||||||
|
Attribute: Object.keys(response).map(k => ({
|
||||||
Name: k,
|
Name: k,
|
||||||
Value: response[k],
|
Value: response[k],
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,18 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
|
|
||||||
import { Action } from '../action.enum';
|
|
||||||
import * as Joi from 'joi';
|
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
|
||||||
|
|
||||||
type QueryParams = {
|
import { Format } from '../abstract-action.handler';
|
||||||
}
|
import { Action } from '../action.enum';
|
||||||
|
import { V2ListQueuesHandler } from './v2-list-queues.handler';
|
||||||
|
import { RequestContext } from '../_context/request.context';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListQueuesHandler extends AbstractActionHandler<QueryParams> {
|
export class ListQueuesHandler extends V2ListQueuesHandler {
|
||||||
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(SqsQueue)
|
|
||||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
|
||||||
) {
|
|
||||||
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 queues = await this.sqsQueueRepo.find({ where: { accountId: awsProperties.accountId }});
|
return { QueueUrl: response.QueueUrls }
|
||||||
|
|
||||||
return {
|
|
||||||
QueueUrl: queues.map((q) => q.getUrl(awsProperties.host))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
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 * as Joi from 'joi';
|
|
||||||
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
import { AttributesService } from '../aws-shared-entities/attributes.service';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
|
|
||||||
type QueryParams = {
|
type QueryParams = {
|
||||||
'Attribute.Name': string;
|
'Attribute.Name': string;
|
||||||
|
|
@ -17,9 +17,8 @@ type QueryParams = {
|
||||||
export class SetQueueAttributesHandler extends AbstractActionHandler<QueryParams> {
|
export class SetQueueAttributesHandler extends AbstractActionHandler<QueryParams> {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SqsQueue)
|
|
||||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
|
||||||
private readonly attributeService: AttributesService,
|
private readonly attributeService: AttributesService,
|
||||||
|
private readonly sqsQueueEntryService: SqsQueueEntryService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -32,9 +31,9 @@ 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.sqsQueueRepo.findOne({ where: { accountId , name } });
|
const queue = await this.sqsQueueEntryService.findQueueByAccountIdAndName(accountId, name);
|
||||||
const attributes = SqsQueue.attributePairs(params);
|
const attributes = SqsQueue.attributePairs(params);
|
||||||
|
|
||||||
if (params['Attribute.Name'] && params['Attribute.Value']) {
|
if (params['Attribute.Name'] && params['Attribute.Value']) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { Prisma, SqsQueueMessage } from '@prisma/client';
|
||||||
import { Repository } from 'typeorm';
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
import { PrismaService } from '../_prisma/prisma.service';
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
import { SqsQueue } from './sqs-queue.entity';
|
||||||
import * as uuid from 'uuid';
|
import { QueueNameExists } from '../aws-shared-entities/aws-exceptions';
|
||||||
|
|
||||||
type QueueEntry = {
|
type QueueEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -20,94 +22,119 @@ const FIFTEEN_SECONDS = 15 * 1000;
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SqsQueueEntryService {
|
export class SqsQueueEntryService {
|
||||||
|
|
||||||
// Heavy use may require event-driven locking implementation
|
|
||||||
private queues: Record<string, QueueEntry[]> = {};
|
|
||||||
|
|
||||||
private queueObjectCache: Record<string, [Date, SqsQueue]> = {};
|
private queueObjectCache: Record<string, [Date, SqsQueue]> = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(SqsQueue)
|
private readonly prismaService: PrismaService,
|
||||||
private readonly sqsQueueRepo: Repository<SqsQueue>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findQueueByAccountIdAndName(accountId: string, name: string): Promise<SqsQueue> {
|
async findQueueByAccountIdAndName(accountId: string, name: string): Promise<SqsQueue | null> {
|
||||||
return await this.sqsQueueRepo.findOne({ where: { accountId, name } });
|
const prisma = await this.prismaService.sqsQueue.findFirst({ where: { accountId, name } });
|
||||||
|
return prisma ? new SqsQueue(prisma) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics(queueArn: string): Metrics {
|
async createQueue(data: Prisma.SqsQueueCreateInput): Promise<SqsQueue> {
|
||||||
|
try {
|
||||||
|
const prisma = await this.prismaService.sqsQueue.create({ data });
|
||||||
|
return new SqsQueue(prisma);
|
||||||
|
} catch (error) {
|
||||||
|
throw new QueueNameExists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteQueue(id: number): Promise<void> {
|
||||||
|
await this.prismaService.sqsQueue.delete({ where: { id }});
|
||||||
|
}
|
||||||
|
|
||||||
|
async metrics(queueId: number): Promise<Metrics> {
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return this.getQueueList(queueArn).reduce<Metrics>((acc, e) => {
|
const [total, inFlight] = await Promise.all([
|
||||||
acc.total += 1;
|
this.prismaService.sqsQueueMessage.count({ where: { queueId }}),
|
||||||
acc.inFlight += e.inFlightReleaseDate > now ? 1 : 0;
|
this.prismaService.sqsQueueMessage.count({ where: { queueId, inFlightRelease: { gt: now } }}),
|
||||||
return acc;
|
]);
|
||||||
}, { total: 0, inFlight: 0 });
|
|
||||||
|
return { total, inFlight }
|
||||||
}
|
}
|
||||||
|
|
||||||
async publish(accountId: string, queueName: string, message: string) {
|
async publish(accountId: string, queueName: string, message: string) {
|
||||||
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
const prisma = await this.prismaService.sqsQueue.findFirst({ where: { accountId, name: queueName }});
|
||||||
|
|
||||||
if (!queue) {
|
if (!prisma) {
|
||||||
console.warn(`Warning bad subscription to ${queueName}`);
|
console.warn(`Warning bad subscription to ${queueName}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getQueueList(queue.arn).push({
|
const queue = new SqsQueue(prisma);
|
||||||
id: uuid.v4(),
|
|
||||||
queueArn: queue.arn,
|
await this.prismaService.sqsQueueMessage.create({
|
||||||
|
data: {
|
||||||
|
id: randomUUID(),
|
||||||
|
queueId: queue.id,
|
||||||
senderId: accountId,
|
senderId: accountId,
|
||||||
message,
|
message,
|
||||||
inFlightReleaseDate: new Date(),
|
inFlightRelease: new Date(),
|
||||||
createdAt: new Date(),
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async receiveMessages(accountId: string, queueName: string, maxNumberOfMessages = 10, visabilityTimeout = 0): Promise<QueueEntry[]> {
|
async receiveMessages(accountId: string, queueName: string, maxNumberOfMessages = 10, visabilityTimeout = 0): Promise<SqsQueueMessage[]> {
|
||||||
|
|
||||||
const queue = await this.getQueueHelper(accountId, queueName);
|
const queue = await this.getQueueHelper(accountId, queueName);
|
||||||
|
|
||||||
const accessDate = new Date();
|
const accessDate = new Date();
|
||||||
const newInFlightReleaseDate = new Date(accessDate);
|
const newInFlightReleaseDate = new Date(accessDate);
|
||||||
newInFlightReleaseDate.setSeconds(accessDate.getSeconds() + visabilityTimeout);
|
newInFlightReleaseDate.setSeconds(accessDate.getSeconds() + visabilityTimeout);
|
||||||
const records = this.getQueueList(queue.arn).filter(e => e.inFlightReleaseDate <= accessDate).slice(0, maxNumberOfMessages - 1);
|
const records = await this.prismaService.sqsQueueMessage.findMany({
|
||||||
records.forEach(e => e.inFlightReleaseDate = newInFlightReleaseDate);
|
where: {
|
||||||
return records;
|
queueId: queue.id,
|
||||||
|
inFlightRelease: {
|
||||||
|
lte: accessDate,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
take: maxNumberOfMessages,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prismaService.sqsQueueMessage.updateMany({
|
||||||
|
data: {
|
||||||
|
inFlightRelease: newInFlightReleaseDate
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: records.map(r => r.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return records.map(r => ({ ...r, inFlightRelease: newInFlightReleaseDate }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteMessage(accountId: string, queueName: string, id: string): Promise<void> {
|
async deleteMessage(id: string): Promise<void> {
|
||||||
|
await this.prismaService.sqsQueueMessage.delete({ where: { id }});
|
||||||
const queue = await this.getQueueHelper(accountId, queueName);
|
|
||||||
|
|
||||||
const records = this.getQueueList(queue.arn);
|
|
||||||
const loc = records.findIndex(r => r.id === id);
|
|
||||||
records.splice(loc, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async purge(accountId: string, queueName: string) {
|
async purge(accountId: string, queueName: string) {
|
||||||
const queue = await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }});
|
const queue = await this.findQueueByAccountIdAndName(accountId, queueName);
|
||||||
this.queues[queue.arn] = [];
|
|
||||||
|
if (!queue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prismaService.sqsQueueMessage.deleteMany({ where: { queueId: queue.id }});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getQueueHelper(accountId: string, queueName: string): Promise<SqsQueue> {
|
private async getQueueHelper(accountId: string, queueName: string): Promise<SqsQueue> {
|
||||||
if (!this.queueObjectCache[`${accountId}/${queueName}`] || this.queueObjectCache[`${accountId}/${queueName}`][0] < new Date()) {
|
if (!this.queueObjectCache[`${accountId}/${queueName}`] || this.queueObjectCache[`${accountId}/${queueName}`][0] < new Date()) {
|
||||||
this.queueObjectCache[`${accountId}/${queueName}`] = [new Date(Date.now() + FIFTEEN_SECONDS), await this.sqsQueueRepo.findOne({ where: { accountId, name: queueName }})];
|
const queue = await this.findQueueByAccountIdAndName(accountId, queueName);
|
||||||
}
|
|
||||||
|
|
||||||
const [_, queue] = this.queueObjectCache[`${accountId}/${queueName}`];
|
|
||||||
|
|
||||||
if (!queue) {
|
if (!queue) {
|
||||||
throw new BadRequestException('Queue not found');
|
throw new BadRequestException('Queue not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.queueObjectCache[`${accountId}/${queueName}`] = [new Date(Date.now() + FIFTEEN_SECONDS), queue];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, queue] = this.queueObjectCache[`${accountId}/${queueName}`];
|
||||||
return queue;
|
return queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getQueueList(arn: string): QueueEntry[] {
|
|
||||||
|
|
||||||
if (!this.queues[arn]) {
|
|
||||||
this.queues[arn] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.queues[arn];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
import { SqsQueue as PrismaSqsQueue } from '@prisma/client';
|
||||||
|
|
||||||
import { getPathFromUrl } from '../util/get-path-from-url';
|
import { getPathFromUrl } from '../util/get-path-from-url';
|
||||||
|
|
||||||
const attributeSlotMap = {
|
const attributeSlotMap = {
|
||||||
|
|
@ -6,23 +7,24 @@ const attributeSlotMap = {
|
||||||
'Value': 'value',
|
'Value': 'value',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity('sqs_queue')
|
export class SqsQueue implements PrismaSqsQueue {
|
||||||
export class SqsQueue extends BaseEntity {
|
|
||||||
|
|
||||||
@PrimaryColumn({ name: 'name' })
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ name: 'account_id', nullable: false })
|
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
|
||||||
@Column({ name: 'region', nullable: false })
|
|
||||||
region: string;
|
region: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
@CreateDateColumn()
|
constructor(p: PrismaSqsQueue) {
|
||||||
createdAt: string;
|
this.id = p.id;
|
||||||
|
this.name = p.name;
|
||||||
|
this.accountId = p.accountId;
|
||||||
|
this.region = p.region;
|
||||||
|
this.createdAt = p.createdAt;
|
||||||
|
this.updatedAt = p.updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: string;
|
|
||||||
|
|
||||||
get arn(): string {
|
get arn(): string {
|
||||||
return `arn:aws:sqs:${this.region}:${this.accountId}:${this.name}`;
|
return `arn:aws:sqs:${this.region}:${this.accountId}:${this.name}`;
|
||||||
|
|
@ -39,8 +41,8 @@ export class SqsQueue extends BaseEntity {
|
||||||
|
|
||||||
static getAccountIdAndNameFromArn(arn: string): [string, string] {
|
static getAccountIdAndNameFromArn(arn: string): [string, string] {
|
||||||
const parts = arn.split(':');
|
const parts = arn.split(':');
|
||||||
const name = parts.pop();
|
const name = parts.pop() as string;
|
||||||
const accountId = parts.pop();
|
const accountId = parts.pop() as string;
|
||||||
return [accountId, name];
|
return [accountId, name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,19 +54,42 @@ export class SqsQueue extends BaseEntity {
|
||||||
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 }[] {
|
||||||
const pairs = [null];
|
|
||||||
for (const param of Object.keys(queryParams)) {
|
if (queryParams.Attributes) {
|
||||||
const [type, idx, slot] = param.split('.');
|
return Object.entries(queryParams.Attributes as Record<string, string>).map(([key, value]) => ({ key, value }));
|
||||||
if (type === 'Attribute') {
|
|
||||||
if (!pairs[+idx]) {
|
|
||||||
pairs[+idx] = { key: '', value: ''};
|
|
||||||
}
|
}
|
||||||
pairs[+idx][attributeSlotMap[slot]] = queryParams[param];
|
|
||||||
|
const pairs: { key: string, value: string }[] = [];
|
||||||
|
for (const param of Object.keys(queryParams)) {
|
||||||
|
const components = this.breakdownAwsQueryParam(param);
|
||||||
|
|
||||||
|
if (!components) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [type, idx, slot] = components;
|
||||||
|
|
||||||
|
if (type === 'Attribute') {
|
||||||
|
if (!pairs[idx]) {
|
||||||
|
pairs[idx] = { key: '', value: ''};
|
||||||
|
}
|
||||||
|
pairs[+idx][slot] = queryParams[param];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pairs.shift();
|
|
||||||
return pairs;
|
return pairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static breakdownAwsQueryParam(paramKey: string): [string, number, 'key' | 'value'] | null {
|
||||||
|
|
||||||
|
const parts = paramKey.split('.');
|
||||||
|
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [type, idx, slot] = parts;
|
||||||
|
return [type, +idx, attributeSlotMap[slot as 'Name' | 'Value'] as 'key' | 'value'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { Format } from '../abstract-action.handler';
|
import { Format } from '../abstract-action.handler';
|
||||||
import { Action } from '../action.enum';
|
import { Action } from '../action.enum';
|
||||||
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
|
||||||
|
|
@ -14,9 +14,11 @@ import { PurgeQueueHandler } from './purge-queue.handler';
|
||||||
import { ReceiveMessageHandler } from './receive-message.handler';
|
import { ReceiveMessageHandler } from './receive-message.handler';
|
||||||
import { SetQueueAttributesHandler } from './set-queue-attributes.handler';
|
import { SetQueueAttributesHandler } from './set-queue-attributes.handler';
|
||||||
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
import { SqsQueueEntryService } from './sqs-queue-entry.service';
|
||||||
import { SqsQueue } from './sqs-queue.entity';
|
|
||||||
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 { 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,12 +55,32 @@ 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({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([SqsQueue]),
|
|
||||||
AwsSharedEntitiesModule,
|
AwsSharedEntitiesModule,
|
||||||
|
PrismaModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
...handlers,
|
...handlers,
|
||||||
|
|
|
||||||
|
|
@ -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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue