Sts addition, kms updates, context object, improved exception handling

This commit is contained in:
Matthew Bessette 2024-12-20 01:07:33 -05:00
parent 095ecbd643
commit c34ea76e4e
50 changed files with 3129 additions and 1149 deletions

2514
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
"sqlite3": "^5.1.6" "sqlite3": "^5.1.6"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^10.4.9",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/joi": "^17.2.2", "@types/joi": "^17.2.2",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",

Binary file not shown.

View File

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

View File

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

View File

@ -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"

View File

@ -4,7 +4,7 @@ generator client {
datasource db { datasource db {
provider = "sqlite" provider = "sqlite"
url = ":memory:" url = "file:local-aws-state.sqlite"
} }
model Attribute { model Attribute {
@ -24,6 +24,26 @@ model Audit {
response String? response String?
} }
model KmsAlias {
name String
accountId String
region String
kmsKeyId String
@@id([accountId, region, name])
}
model KmsKey {
id String @id
usage String
description String
keySpec String
key String
accountId String
region String
createdAt DateTime @default(now())
}
model Secret { model Secret {
versionId String @id versionId String @id
name String name String

View File

@ -0,0 +1,102 @@
import { CallHandler, ExecutionContext, HttpException, Inject, Injectable, Logger, NestInterceptor, RequestTimeoutException } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { catchError, Observable, tap, throwError } from 'rxjs';
import { Request as ExpressRequest, Response } from 'express';
import * as Joi from 'joi';
import { PrismaService } from '../_prisma/prisma.service';
import { ActionHandlers } from '../app.constants';
import { Action } from '../action.enum';
import { Format } from '../abstract-action.handler';
import { AwsException, InternalFailure } from '../aws-shared-entities/aws-exceptions';
import { IRequest, RequestContext } from './request.context';
@Injectable()
export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
private readonly logger = new Logger(AuditInterceptor.name);
constructor(
@Inject(ActionHandlers)
private readonly handlers: ActionHandlers,
private readonly prismaService: PrismaService,
) {}
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
const requestContext: RequestContext = {
requestId: randomUUID(),
}
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(
catchError(async (error: 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),
}
});
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),
}
}),
})
);
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,20 @@
import { Request } from "express";
import { Action } from "../action.enum";
import { Format } from "../abstract-action.handler";
export interface RequestContext {
action?: Action;
format?: Format;
requestId: string;
}
export interface IRequest extends Request {
context: RequestContext;
headers: {
'x-amz-target'?: string;
},
body: {
'Action'?: string;
}
}

View File

@ -4,7 +4,5 @@ import { PrismaClient } from "@prisma/client";
export class PrismaService extends PrismaClient implements OnModuleInit { export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() { async onModuleInit() {
await this.$connect(); await this.$connect();
const tables = await this.$queryRawUnsafe('.tables');
console.log({ tables })
} }
} }

View File

@ -301,4 +301,15 @@ export enum Action {
SqsSetQueueAttributes = 'SetQueueAttributes', SqsSetQueueAttributes = 'SetQueueAttributes',
SqsTagQueue = 'TagQueue', SqsTagQueue = 'TagQueue',
SqsUntagQueue = 'UntagQueue', SqsUntagQueue = 'UntagQueue',
// STS
StsAssumeRole = 'AssumeRole',
StsAssumeRoleWithSaml = 'AssumeRoleWithSaml',
StsAssumeRoleWithWebIdentity = 'AssumeRoleWithWebIdentity',
StsAssumeRoot = 'AssumeRoot',
StsDecodeAuthorizationMessage = 'DecodeAuthorizationMessage',
StsGetAccessKeyInfo = 'GetAccessKeyInfo',
StsGetCallerIdentity = 'GetCallerIdentity',
StsGetFederationToken = 'GetFederationToken',
StsGetSessionToken = 'GetSessionToken',
} }

View File

@ -1,13 +1,19 @@
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 { AuditInterceptor } from './audit/audit.interceptor'; 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 './_context/audit.interceptor';
import { CommonConfig } from './config/common-config.interface';
import { InvalidAction, ValidationError } from './aws-shared-entities/aws-exceptions';
type QueryParams = {
__path: string;
} & Record<string, string>;
@Controller() @Controller()
export class AppController { export class AppController {
@ -30,24 +36,24 @@ 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 awsProperties = {

View File

@ -3,11 +3,9 @@ import { ConfigModule } from '@nestjs/config';
import { ActionHandlers } from './app.constants'; import { ActionHandlers } from './app.constants';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AuditInterceptor } from './audit/audit.interceptor'; import { AuditInterceptor } from './_context/audit.interceptor';
import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module'; import { AwsSharedEntitiesModule } from './aws-shared-entities/aws-shared-entities.module';
import localConfig from './config/local.config'; import localConfig from './config/local.config';
import { IAMHandlers } from './iam/iam.constants';
import { IamModule } from './iam/iam.module';
import { KMSHandlers } from './kms/kms.constants'; import { KMSHandlers } from './kms/kms.constants';
import { KmsModule } from './kms/kms.module'; import { KmsModule } from './kms/kms.module';
import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants'; import { SecretsManagerHandlers } from './secrets-manager/secrets-manager.constants';
@ -16,6 +14,9 @@ import { SnsHandlers } from './sns/sns.constants';
import { SnsModule } from './sns/sns.module'; import { SnsModule } from './sns/sns.module';
import { SqsHandlers } from './sqs/sqs.constants'; import { SqsHandlers } from './sqs/sqs.constants';
import { SqsModule } from './sqs/sqs.module'; import { SqsModule } from './sqs/sqs.module';
import { PrismaModule } from './_prisma/prisma.module';
import { StsModule } from './sts/sts.module';
import { StsHandlers } from './sts/sts.constants';
@Module({ @Module({
imports: [ imports: [
@ -23,12 +24,13 @@ import { SqsModule } from './sqs/sqs.module';
load: [localConfig], load: [localConfig],
isGlobal: true, isGlobal: true,
}), }),
IamModule, PrismaModule,
AwsSharedEntitiesModule,
KmsModule, KmsModule,
SecretsManagerModule, SecretsManagerModule,
SnsModule, SnsModule,
SqsModule, SqsModule,
AwsSharedEntitiesModule, StsModule,
], ],
controllers: [ controllers: [
AppController, AppController,
@ -39,11 +41,11 @@ import { SqsModule } from './sqs/sqs.module';
provide: ActionHandlers, provide: ActionHandlers,
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}), useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
inject: [ inject: [
KMSHandlers,
SecretsManagerHandlers,
SnsHandlers, SnsHandlers,
SqsHandlers, SqsHandlers,
SecretsManagerHandlers, StsHandlers,
KMSHandlers,
IAMHandlers,
], ],
}, },
], ],

View File

@ -1,69 +0,0 @@
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { Observable, tap } from 'rxjs';
import { Request as ExpressRequest } from 'express';
import * as Joi from 'joi';
import { PrismaService } from '../_prisma/prisma.service';
import { ActionHandlers } from '../app.constants';
import { Action } from '../action.enum';
interface Request extends ExpressRequest {
headers: {
'x-amz-target'?: string;
},
body: {
'Action'?: string;
}
}
@Injectable()
export class AuditInterceptor<T> implements NestInterceptor<T, Response> {
constructor(
@Inject(ActionHandlers)
private readonly handlers: ActionHandlers,
private readonly prismaService: PrismaService,
) {}
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<any> {
const requestId = randomUUID();
const httpContext = context.switchToHttp();
const request = httpContext.getRequest<Request>();
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 };
const response = context.switchToHttp().getResponse();
response.header('x-amzn-RequestId', requestId);
if (!resolvedAction || !this.handlers[resolvedAction]?.audit) {
return next.handle();
}
return next.handle().pipe(
tap({
next: async (data) => await this.prismaService.audit.create({
data: {
id: 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: requestId,
action,
request: JSON.stringify({ __path: request.path, ...request.headers, ...request.body }),
response: JSON.stringify(error),
}
}),
})
);
}
}

View File

@ -0,0 +1,156 @@
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,
)
}
}

View File

@ -1,47 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
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 { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
import { IamRole } from './iam-role.entity';
type QueryParams = {
PolicyArn: string;
RoleName: string;
}
@Injectable()
export class AttachRolePolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamRole)
private readonly roleRepo: Repository<IamRole>,
@InjectRepository(IamRolePolicyAttachment)
private readonly attachRepo: Repository<IamRolePolicyAttachment>,
) {
super();
}
format = Format.Xml;
action = Action.IamAttachRolePolicy;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
RoleName: Joi.string().required(),
});
protected async handle({ PolicyArn, RoleName }: QueryParams, awsProperties: AwsProperties) {
const role = await this.roleRepo.findOne({ where: { roleName: RoleName, accountId: awsProperties.accountId} });
await this.attachRepo.create({
id: uuid.v4(),
policyArn: PolicyArn,
roleId: role.id,
accountId: awsProperties.accountId,
}).save();
}
}

View File

@ -1,62 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
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 { breakdownArn } from '../util/breakdown-arn';
type QueryParams = {
PolicyArn: string;
PolicyDocument: string;
SetAsDefault: boolean;
}
@Injectable()
export class CreatePolicyVersionHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamPolicy)
private readonly policyRepo: Repository<IamPolicy>,
) {
super();
}
format = Format.Xml;
action = Action.IamCreatePolicyVersion;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
PolicyDocument: Joi.string().required(),
SetAsDefault: Joi.boolean().required(),
});
protected async handle({ PolicyArn, PolicyDocument, SetAsDefault }: QueryParams, awsProperties: AwsProperties) {
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(),
}
}
}
}

View File

@ -1,54 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
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';
type QueryParams = {
PolicyName: string;
PolicyDocument: string;
}
@Injectable()
export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamPolicy)
private readonly policyRepo: Repository<IamPolicy>,
) {
super();
}
format = Format.Xml;
action = Action.IamCreatePolicy;
validator = Joi.object<QueryParams, true>({
PolicyName: Joi.string().required(),
PolicyDocument: Joi.string().required(),
});
protected async handle({ PolicyName, PolicyDocument }: QueryParams, awsProperties: AwsProperties) {
const policy = await this.policyRepo.create({
id: uuid.v4(),
name: PolicyName,
document: PolicyDocument,
accountId: awsProperties.accountId,
}).save();
return {
Policy: {
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(),
}
}
}
}

View File

@ -1,65 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IamRole } from './iam-role.entity';
import * as uuid from 'uuid';
import { IamPolicy } from './iam-policy.entity';
type QueryParams = {
RoleName: string;
Path: string;
AssumeRolePolicyDocument: string;
MaxSessionDuration: number;
}
@Injectable()
export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamRole)
private readonly roleRepo: Repository<IamRole>,
@InjectRepository(IamPolicy)
private readonly policyRepo: Repository<IamPolicy>,
) {
super();
}
format = Format.Xml;
action = Action.IamCreateRole;
validator = Joi.object<QueryParams, true>({
RoleName: Joi.string().required(),
Path: Joi.string().required(),
AssumeRolePolicyDocument: Joi.string().required(),
MaxSessionDuration: Joi.number().default(3600),
});
protected async handle({ RoleName, Path, AssumeRolePolicyDocument, MaxSessionDuration }: QueryParams, awsProperties: AwsProperties) {
const policy = await this.policyRepo.create({
id: uuid.v4(),
name: `${RoleName}-AssumeRolePolicyDocument`,
document: AssumeRolePolicyDocument,
accountId: awsProperties.accountId,
}).save();
const id = uuid.v4();
await this.roleRepo.create({
id,
roleName: RoleName,
path: Path,
accountId: awsProperties.accountId,
assumeRolePolicyDocumentId: policy.id,
maxSessionDuration: MaxSessionDuration,
}).save();
const role = await this.roleRepo.findOne({ where: { id }});
return {
Role: role.metadata,
}
}
}

View File

@ -1,54 +0,0 @@
import { Injectable, NotFoundException, Version } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IamPolicy } from './iam-policy.entity';
import { breakdownArn } from '../util/breakdown-arn';
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
type QueryParams = {
PolicyArn: string;
VersionId: string;
}
@Injectable()
export class GetPolicyVersionHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamPolicy)
private readonly policyRepo: Repository<IamPolicy>,
@InjectRepository(IamRolePolicyAttachment)
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
) {
super();
}
format = Format.Xml;
action = Action.IamGetPolicyVersion;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
VersionId: Joi.string().required(),
});
protected async handle({ PolicyArn, VersionId }: QueryParams, awsProperties: AwsProperties) {
const { identifier, accountId } = breakdownArn(PolicyArn);
const [_policy, name] = identifier.split('/');
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 {
PolicyVersion: {
Document: policy.document,
IsDefaultVersion: policy.isDefault,
VersionId: `${policy.version}`,
CreateDate: new Date(policy.createdAt).toISOString(),
}
}
}
}

View File

@ -1,58 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IamPolicy } from './iam-policy.entity';
import { breakdownArn } from '../util/breakdown-arn';
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
type QueryParams = {
PolicyArn: string;
}
@Injectable()
export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamPolicy)
private readonly policyRepo: Repository<IamPolicy>,
@InjectRepository(IamRolePolicyAttachment)
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
) {
super();
}
format = Format.Xml;
action = Action.IamGetPolicy;
validator = Joi.object<QueryParams, true>({
PolicyArn: Joi.string().required(),
});
protected async handle({ PolicyArn }: QueryParams, awsProperties: AwsProperties) {
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 {
Policy: {
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(),
}
}
}
}

View File

@ -1,41 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IamRole } from './iam-role.entity';
type QueryParams = {
RoleName: string;
}
@Injectable()
export class GetRoleHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamRole)
private readonly roleRepo: Repository<IamRole>,
) {
super();
}
format = Format.Xml;
action = Action.IamGetRole;
validator = Joi.object<QueryParams, true>({
RoleName: Joi.string().required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
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 {
Role: role.metadata,
}
}
}

View File

@ -1,38 +0,0 @@
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, OneToMany, OneToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
import { IamRole } from './iam-role.entity';
@Entity({ name: 'iam_policy' })
export class IamPolicy extends BaseEntity {
@PrimaryColumn()
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;
@CreateDateColumn()
createdAt: string;
@UpdateDateColumn()
updatedAt: string;
@OneToOne(() => IamRole, role => role.assumeRolePolicyDocument)
iamRole: IamRole;
get arn() {
return `arn:aws:iam::${this.accountId}:policy/${this.name}`;
}
}

View File

@ -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;
}

View File

@ -1,52 +0,0 @@
import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
import { IamPolicy } from './iam-policy.entity';
@Entity({ name: 'iam_role' })
export class IamRole extends BaseEntity {
@PrimaryColumn()
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;
@Column({ name: 'max_session_duration', nullable: false, default: 0 })
maxSessionDuration: number;
@CreateDateColumn()
createdAt: string;
@UpdateDateColumn()
updatedAt: string;
@OneToOne(() => IamPolicy, (policy) => policy.id, { eager: true })
@JoinColumn({ name: 'assume_role_policy_document_id' })
assumeRolePolicyDocument: IamPolicy;
get arn() {
const identifier = this.path.split('/');
identifier.push(this.roleName);
return `arn:aws:iam::${this.accountId}:role/${identifier.join('/')}`;
}
get metadata() {
return {
Path: this.path,
Arn: this.arn,
RoleName: this.roleName,
AssumeRolePolicyDocument: this.assumeRolePolicyDocument.document,
CreateDate: new Date(this.createdAt).toISOString(),
RoleId: this.id,
MaxSessionDuration: this.maxSessionDuration,
}
}
}

View File

@ -1,5 +0,0 @@
import { AbstractActionHandler } from '../abstract-action.handler';
import { Action } from '../action.enum';
export type IAMHandlers = Record<Action, AbstractActionHandler>;
export const IAMHandlers = Symbol.for('IAMHandlers');

View File

@ -1,207 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
import { AttachRolePolicyHandler } from './attach-role-policy.handler';
import { CreatePolicyVersionHandler } from './create-policy-version.handler';
import { CreatePolicyHandler } from './create-policy.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 { ListAttachedRolePoliciesHandler } from './list-attached-role-policies';
import { ListRolePoliciesHandler } from './list-role-policies.handler';
const handlers = [
AttachRolePolicyHandler,
CreatePolicyHandler,
CreatePolicyVersionHandler,
CreateRoleHandler,
GetPolicyHandler,
GetRoleHandler,
GetPolicyVersionHandler,
ListAttachedRolePoliciesHandler,
ListRolePoliciesHandler,
]
const actions = [
Action.IamAddClientIDToOpenIDConnectProvider,
Action.IamAddRoleToInstanceProfile,
Action.IamAddUserToGroup,
Action.IamAttachGroupPolicy,
Action.IamAttachRolePolicy,
Action.IamAttachUserPolicy,
Action.IamChangePassword,
Action.IamCreateAccessKey,
Action.IamCreateAccountAlias,
Action.IamCreateGroup,
Action.IamCreateInstanceProfile,
Action.IamCreateLoginProfile,
Action.IamCreateOpenIDConnectProvider,
Action.IamCreatePolicy,
Action.IamCreatePolicyVersion,
Action.IamCreateRole,
Action.IamCreateSAMLProvider,
Action.IamCreateServiceLinkedRole,
Action.IamCreateServiceSpecificCredential,
Action.IamCreateUser,
Action.IamCreateVirtualMFADevice,
Action.IamDeactivateMFADevice,
Action.IamDeleteAccessKey,
Action.IamDeleteAccountAlias,
Action.IamDeleteAccountPasswordPolicy,
Action.IamDeleteGroup,
Action.IamDeleteGroupPolicy,
Action.IamDeleteInstanceProfile,
Action.IamDeleteLoginProfile,
Action.IamDeleteOpenIDConnectProvider,
Action.IamDeletePolicy,
Action.IamDeletePolicyVersion,
Action.IamDeleteRole,
Action.IamDeleteRolePermissionsBoundary,
Action.IamDeleteRolePolicy,
Action.IamDeleteSAMLProvider,
Action.IamDeleteServerCertificate,
Action.IamDeleteServiceLinkedRole,
Action.IamDeleteServiceSpecificCredential,
Action.IamDeleteSigningCertificate,
Action.IamDeleteSSHPublicKey,
Action.IamDeleteUser,
Action.IamDeleteUserPermissionsBoundary,
Action.IamDeleteUserPolicy,
Action.IamDeleteVirtualMFADevice,
Action.IamDetachGroupPolicy,
Action.IamDetachRolePolicy,
Action.IamDetachUserPolicy,
Action.IamEnableMFADevice,
Action.IamGenerateCredentialReport,
Action.IamGenerateOrganizationsAccessReport,
Action.IamGenerateServiceLastAccessedDetails,
Action.IamGetAccessKeyLastUsed,
Action.IamGetAccountAuthorizationDetails,
Action.IamGetAccountPasswordPolicy,
Action.IamGetAccountSummary,
Action.IamGetContextKeysForCustomPolicy,
Action.IamGetContextKeysForPrincipalPolicy,
Action.IamGetCredentialReport,
Action.IamGetGroup,
Action.IamGetGroupPolicy,
Action.IamGetInstanceProfile,
Action.IamGetLoginProfile,
Action.IamGetOpenIDConnectProvider,
Action.IamGetOrganizationsAccessReport,
Action.IamGetPolicy,
Action.IamGetPolicyVersion,
Action.IamGetRole,
Action.IamGetRolePolicy,
Action.IamGetSAMLProvider,
Action.IamGetServerCertificate,
Action.IamGetServiceLastAccessedDetails,
Action.IamGetServiceLastAccessedDetailsWithEntities,
Action.IamGetServiceLinkedRoleDeletionStatus,
Action.IamGetSSHPublicKey,
Action.IamGetUser,
Action.IamGetUserPolicy,
Action.IamListAccessKeys,
Action.IamListAccountAliases,
Action.IamListAttachedGroupPolicies,
Action.IamListAttachedRolePolicies,
Action.IamListAttachedUserPolicies,
Action.IamListEntitiesForPolicy,
Action.IamListGroupPolicies,
Action.IamListGroups,
Action.IamListGroupsForUser,
Action.IamListInstanceProfiles,
Action.IamListInstanceProfilesForRole,
Action.IamListInstanceProfileTags,
Action.IamListMFADevices,
Action.IamListMFADeviceTags,
Action.IamListOpenIDConnectProviders,
Action.IamListOpenIDConnectProviderTags,
Action.IamListPolicies,
Action.IamListPoliciesGrantingServiceAccess,
Action.IamListPolicyTags,
Action.IamListPolicyVersions,
Action.IamListRolePolicies,
Action.IamListRoles,
Action.IamListRoleTags,
Action.IamListSAMLProviders,
Action.IamListSAMLProviderTags,
Action.IamListServerCertificates,
Action.IamListServerCertificateTags,
Action.IamListServiceSpecificCredentials,
Action.IamListSigningCertificates,
Action.IamListSSHPublicKeys,
Action.IamListUserPolicies,
Action.IamListUsers,
Action.IamListUserTags,
Action.IamListVirtualMFADevices,
Action.IamPutGroupPolicy,
Action.IamPutRolePermissionsBoundary,
Action.IamPutRolePolicy,
Action.IamPutUserPermissionsBoundary,
Action.IamPutUserPolicy,
Action.IamRemoveClientIDFromOpenIDConnectProvider,
Action.IamRemoveRoleFromInstanceProfile,
Action.IamRemoveUserFromGroup,
Action.IamResetServiceSpecificCredential,
Action.IamResyncMFADevice,
Action.IamSetDefaultPolicyVersion,
Action.IamSetSecurityTokenServicePreferences,
Action.IamSimulateCustomPolicy,
Action.IamSimulatePrincipalPolicy,
Action.IamTagInstanceProfile,
Action.IamTagMFADevice,
Action.IamTagOpenIDConnectProvider,
Action.IamTagPolicy,
Action.IamTagRole,
Action.IamTagSAMLProvider,
Action.IamTagServerCertificate,
Action.IamTagUser,
Action.IamUntagInstanceProfile,
Action.IamUntagMFADevice,
Action.IamUntagOpenIDConnectProvider,
Action.IamUntagPolicy,
Action.IamUntagRole,
Action.IamUntagSAMLProvider,
Action.IamUntagServerCertificate,
Action.IamUntagUser,
Action.IamUpdateAccessKey,
Action.IamUpdateAccountPasswordPolicy,
Action.IamUpdateAssumeRolePolicy,
Action.IamUpdateGroup,
Action.IamUpdateLoginProfile,
Action.IamUpdateOpenIDConnectProviderThumbprint,
Action.IamUpdateRole,
Action.IamUpdateRoleDescription,
Action.IamUpdateSAMLProvider,
Action.IamUpdateServerCertificate,
Action.IamUpdateServiceSpecificCredential,
Action.IamUpdateSigningCertificate,
Action.IamUpdateSSHPublicKey,
Action.IamUpdateUser,
Action.IamUploadServerCertificate,
Action.IamUploadSigningCertificate,
Action.IamUploadSSHPublicKey,
]
@Module({
imports: [
TypeOrmModule.forFeature([IamPolicy, IamRole, IamRolePolicyAttachment]),
AwsSharedEntitiesModule,
],
providers: [
...handlers,
ExistingActionHandlersProvider(handlers),
DefaultActionHandlerProvider(IAMHandlers, Format.Xml, actions),
],
exports: [IAMHandlers],
})
export class IamModule {}

View File

@ -1,57 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
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 = {
RoleName: string;
}
@Injectable()
export class ListAttachedRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamRole)
private readonly roleRepo: Repository<IamRole>,
@InjectRepository(IamPolicy)
private readonly policyRepo: Repository<IamPolicy>,
@InjectRepository(IamRolePolicyAttachment)
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
) {
super();
}
format = Format.Xml;
action = Action.IamListAttachedRolePolicies;
validator = Joi.object<QueryParams, true>({
RoleName: Joi.string().required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
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 {
AttachedPolicies: {
member: [role.assumeRolePolicyDocument, ...policies].map(p => ({
PolicyName: p.name,
PolicyArn: p.arn,
})),
}
}
}
}

View File

@ -1,44 +0,0 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IamRole } from './iam-role.entity';
import { IamRolePolicyAttachment } from './iam-role-policy-attachment.entity';
type QueryParams = {
RoleName: string;
}
@Injectable()
export class ListRolePoliciesHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(IamRole)
private readonly roleRepo: Repository<IamRole>,
@InjectRepository(IamRolePolicyAttachment)
private readonly attachmentRepo: Repository<IamRolePolicyAttachment>,
) {
super();
}
format = Format.Xml;
action = Action.IamListRolePolicies;
validator = Joi.object<QueryParams, true>({
RoleName: Joi.string().required(),
});
protected async handle({ RoleName }: QueryParams, awsProperties: AwsProperties) {
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: [],
}
}
}

View File

@ -1,40 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { KmsKeyAlias } from './kms-key-alias.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
type QueryParams = {
AliasName: string;
TargetKeyId: string;
}
@Injectable()
export class CreateAliasHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(KmsKeyAlias)
private readonly aliasRepo: Repository<KmsKeyAlias>,
) {
super();
}
format = Format.Json;
action = Action.KmsCreateAlias;
validator = Joi.object<QueryParams, true>({
AliasName: Joi.string().required(),
TargetKeyId: Joi.string().required(),
});
protected async handle({ AliasName, TargetKeyId }: QueryParams, awsProperties: AwsProperties) {
await this.aliasRepo.save({
name: AliasName.split('/')[1],
targetKeyId: TargetKeyId,
accountId: awsProperties.accountId,
region: awsProperties.region,
});
}
}

View File

View File

@ -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 { breakdownArn } from '../util/breakdown-arn';
import { KmsService } from './kms.service'; import { KmsService } from './kms.service';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
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,6 +24,7 @@ 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: AwsProperties) {
@ -38,16 +36,17 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
identifier: KeyId, identifier: KeyId,
}; };
const [ type, pk ] = searchable.identifier.split('/'); const [ type, pk ] = searchable.identifier.split('/');
const keyId: Promise<string> = type === 'key' ? const keyId = await (type === 'key' ? Promise.resolve(pk) : this.kmsService.findKeyIdFromAlias(pk, searchable));
Promise.resolve(pk) :
this.kmsService.findKeyIdFromAlias(pk, searchable);
if (!keyId) {
throw new NotFoundException();
}
const keyRecord = await this.keyRepo.findOne({ where: { const keyRecord = await this.kmsService.findOneById(keyId);
id: await keyId,
region: searchable.region, if (!keyRecord) {
accountId: searchable.accountId, throw new NotFoundException();
}}); }
return { return {
KeyMetadata: keyRecord.metadata, KeyMetadata: keyRecord.metadata,

View File

@ -1,123 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
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 * as crypto from 'crypto';
type QueryParams = {
GrantTokens: 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()
export class GetPublicKeyHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(KmsKey)
private readonly keyRepo: Repository<KmsKey>,
private readonly kmsService: KmsService,
) {
super();
}
format = Format.Json;
action = Action.KmsGetPublicKey;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
GrantTokens: Joi.array().items(Joi.string()),
});
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties): Promise<Output> {
const searchable = KeyId.startsWith('arn') ? breakdownArn(KeyId) : {
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);
const keyRecord = await this.keyRepo.findOne({ where: {
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 {
CustomerMasterKeySpec: keyRecord.keySpec,
KeyId: keyRecord.arn,
KeySpec: keyRecord.keySpec,
KeyUsage: keyRecord.usage,
PublicKey: Buffer.from(pubKeyObject.export({
format: 'pem',
type: 'spki',
})).toString('utf-8'),
}
}
}

View File

@ -1,21 +1,11 @@
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; export class KmsKeyAlias {
@Entity({ name: 'kms_key_alias' }) // name: string;
export class KmsKeyAlias extends BaseEntity { // targetKeyId: string;
// accountId: string;
// region: string;
@PrimaryColumn() // get arn() {
name: string; // return `arn:aws:kms:${this.region}:${this.accountId}:alias/${this.name}`;
// }
@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}`;
}
} }

View File

@ -1,34 +1,29 @@
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; 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 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 type KeyUsage = 'SIGN_VERIFY' | 'ENCRYPT_DECRYPT' | 'GENERATE_VERIFY_MAC'; export type KeyUsage = 'SIGN_VERIFY' | 'ENCRYPT_DECRYPT' | 'GENERATE_VERIFY_MAC';
@Entity({ name: 'kms_key'}) export class KmsKey implements PrismaKmsKey {
export class KmsKey extends BaseEntity {
@PrimaryColumn()
id: string; id: string;
@Column({ name: 'usage' })
usage: KeyUsage; usage: KeyUsage;
@Column({ name: 'description' })
description: string; description: string;
@Column({ name: 'key_spec' })
keySpec: KeySpec; keySpec: KeySpec;
@Column({ name: 'key' })
key: string; key: string;
@Column({ name: 'account_id', nullable: false })
accountId: string; accountId: string;
@Column({ name: 'region', nullable: false })
region: string; region: string;
createdAt: Date;
@CreateDateColumn() constructor(p: PrismaKmsKey) {
createdAt: string; this.id = p.id;
this.usage = p.usage as KeyUsage;
this.description = p.description;
this.keySpec = p.keySpec as KeySpec;
this.key = p.key;
this.accountId = p.accountId;
this.region = p.region;
this.createdAt = p.createdAt;
}
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}`;

View File

@ -1,22 +1,17 @@
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 { GetPublicKeyHandler } from './get-public-key.handler'; import { KMSHandlers } from './kms.constants';
import { DescribeKeyHandler } from './describe-key.handler';
import { PrismaModule } from '../_prisma/prisma.module';
const handlers = [ const handlers = [
CreateAliasHandler,
DescribeKeyHandler, DescribeKeyHandler,
GetPublicKeyHandler,
] ]
const actions = [ const actions = [
@ -74,8 +69,8 @@ const actions = [
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([KmsKey, KmsKeyAlias]),
AwsSharedEntitiesModule, AwsSharedEntitiesModule,
PrismaModule,
], ],
providers: [ providers: [
...handlers, ...handlers,

View File

@ -1,22 +1,30 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { PrismaService } from '../_prisma/prisma.service';
import { ArnParts } from '../util/breakdown-arn'; import { ArnParts } from '../util/breakdown-arn';
import { InjectRepository } from '@nestjs/typeorm'; import { KmsKey } from './kms-key.entity';
import { KmsKeyAlias } from './kms-key-alias.entity';
import { Repository } from 'typeorm';
@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 findOneById(id: string): Promise<KmsKey | null> {
const record = await this.aliasRepo.findOne({ where: { const pRecord = await this.prismaService.kmsKey.findFirst({
name: alias, where: { id }
accountId: arn.accountId, });
region: arn.region, return pRecord ? new KmsKey(pRecord) : null;
}}); }
return record.targetKeyId;
async findKeyIdFromAlias(alias: string, arn: ArnParts): Promise<string | null> {
const record = await this.prismaService.kmsAlias.findFirst({
where: {
name: alias,
accountId: arn.accountId,
region: arn.region,
}
});
return record?.kmsKeyId ?? null;
} }
} }

View File

View File

@ -4,13 +4,15 @@ import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { CommonConfig } from './config/common-config.interface'; import { CommonConfig } from './config/common-config.interface';
import { AwsExceptionFilter } from './_context/exception.filter';
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
(async () => { (async () => {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); // app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
app.useGlobalFilters(new AwsExceptionFilter());
app.use(bodyParser.json({ type: 'application/x-amz-json-1.1'})); app.use(bodyParser.json({ type: 'application/x-amz-json-1.1'}));
const configService: ConfigService<CommonConfig, true> = app.get(ConfigService); const configService: ConfigService<CommonConfig, true> = app.get(ConfigService);

View File

@ -6,6 +6,7 @@ import { TagsService } from '../aws-shared-entities/tags.service';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { PrismaService } from '../_prisma/prisma.service'; import { PrismaService } from '../_prisma/prisma.service';
import { ArnUtil } from '../util/arn-util.static'; import { ArnUtil } from '../util/arn-util.static';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = { type QueryParams = {
SubscriptionArn: string; SubscriptionArn: string;
@ -34,7 +35,7 @@ export class UnsubscribeHandler extends AbstractActionHandler<QueryParams> {
const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id } }); const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id } });
if (!subscription) { if (!subscription) {
return; throw new NotFoundException();
} }
const arn = ArnUtil.fromTopicSub(subscription); const arn = ArnUtil.fromTopicSub(subscription);

View File

@ -8,7 +8,7 @@ import { SqsQueue } from './sqs-queue.entity';
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> {
@ -34,7 +34,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);
} }
} }
} }

View File

@ -29,8 +29,6 @@ export class DeleteMessageHandler extends AbstractActionHandler<QueryParams> {
}); });
protected async handle({ QueueUrl, ReceiptHandle }: QueryParams, awsProperties: AwsProperties) { protected async handle({ QueueUrl, ReceiptHandle }: QueryParams, awsProperties: AwsProperties) {
await this.sqsQueueEntryService.deleteMessage(ReceiptHandle);
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
await this.sqsQueueEntryService.deleteMessage(accountId, name, ReceiptHandle);
} }
} }

View File

@ -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,9 @@ 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';
const handlers = [ const handlers = [
CreateQueueHandler, CreateQueueHandler,
@ -55,8 +55,8 @@ const actions = [
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([SqsQueue]),
AwsSharedEntitiesModule, AwsSharedEntitiesModule,
PrismaModule,
], ],
providers: [ providers: [
...handlers, ...handlers,

View File

@ -0,0 +1,23 @@
import { Injectable } from "@nestjs/common";
import * as Joi from "joi";
import { AbstractActionHandler, AwsProperties, Format } from "../abstract-action.handler";
import { Action } from "../action.enum";
type QueryParams = {}
@Injectable()
export class GetCallerIdentityHandler extends AbstractActionHandler<QueryParams> {
format = Format.Xml;
action = Action.StsGetCallerIdentity;
validator = Joi.object<QueryParams, true>();
protected async handle(queryParams: QueryParams, awsProperties: AwsProperties) {
return {
"UserId": "AIDASAMPLEUSERID",
"Account": awsProperties.accountId,
"Arn": `arn:aws:iam::${awsProperties.accountId}:user/DevAdmin`
}
}
}

5
src/sts/sts.constants.ts Normal file
View File

@ -0,0 +1,5 @@
import { AbstractActionHandler } from '../abstract-action.handler';
import { Action } from '../action.enum';
export type StsHandlers = Record<Action, AbstractActionHandler>;
export const StsHandlers = Symbol.for('STS_HANDLERS');

40
src/sts/sts.module.ts Normal file
View File

@ -0,0 +1,40 @@
import { Module } from '@nestjs/common';
import { Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import { PrismaModule } from '../_prisma/prisma.module';
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
import { DefaultActionHandlerProvider } from '../default-action-handler/default-action-handler.provider';
import { ExistingActionHandlersProvider } from '../default-action-handler/existing-action-handlers.provider';
import { GetCallerIdentityHandler } from './get-caller-identity.handler';
import { StsHandlers } from './sts.constants';
const handlers = [
GetCallerIdentityHandler,
]
const actions = [
Action.StsAssumeRole,
Action.StsAssumeRoleWithSaml,
Action.StsAssumeRoleWithWebIdentity,
Action.StsAssumeRoot,
Action.StsDecodeAuthorizationMessage,
Action.StsGetAccessKeyInfo,
Action.StsGetCallerIdentity,
Action.StsGetFederationToken,
Action.StsGetSessionToken,
]
@Module({
imports: [
AwsSharedEntitiesModule,
PrismaModule,
],
providers: [
...handlers,
ExistingActionHandlersProvider(handlers),
DefaultActionHandlerProvider(StsHandlers, Format.Xml, actions),
],
exports: [StsHandlers]
})
export class StsModule {}

0
src/sts/sts.service.ts Normal file
View File

View File

@ -1,3 +1,5 @@
import { InvalidArnException } from "../aws-shared-entities/aws-exceptions";
export type ArnParts = { export type ArnParts = {
service: string; service: string;
region: string; region: string;
@ -7,7 +9,7 @@ export type ArnParts = {
export const breakdownArn = (arn: string): ArnParts => { export const breakdownArn = (arn: string): ArnParts => {
if (!arn.startsWith('arn')) { if (!arn.startsWith('arn')) {
throw new Error('Invalid arn'); throw new InvalidArnException('Invalid arn');
} }
const [_arn, _aws, service, region, accountId, ...identifierData] = arn.split(':'); const [_arn, _aws, service, region, accountId, ...identifierData] = arn.split(':');

View File

@ -1,4 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "dist", "**/*spec.ts"] "exclude": ["node_modules", "dist", "src/iam"]
} }