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

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

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 {
async onModuleInit() {
await this.$connect();
const tables = await this.$queryRawUnsafe('.tables');
console.log({ tables })
}
}

View File

@@ -301,4 +301,15 @@ export enum Action {
SqsSetQueueAttributes = 'SetQueueAttributes',
SqsTagQueue = 'TagQueue',
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 { 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 { BadRequestException, Body, Controller, Headers, HttpCode, Inject, Post, Req, UseInterceptors } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CommonConfig } from './config/common-config.interface';
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()
export class AppController {
@@ -30,24 +36,24 @@ export class AppController {
const lowerCasedHeaders = Object.keys(headers).reduce((o, k) => {
o[k.toLocaleLowerCase()] = headers[k];
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 { error: actionError } = Joi.object({
[actionKey]: Joi.string().valid(...Object.values(Action)).required(),
}).validate(queryParams, { allowUnknown: true });
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 { error: validatorError, value: validQueryParams } = handler.validator.validate(queryParams, { allowUnknown: true, abortEarly: false });
if (validatorError) {
throw new BadRequestException(validatorError.message, { cause: validatorError });
throw new ValidationError(validatorError.message);
}
const awsProperties = {

View File

@@ -3,11 +3,9 @@ import { ConfigModule } from '@nestjs/config';
import { ActionHandlers } from './app.constants';
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 localConfig from './config/local.config';
import { IAMHandlers } from './iam/iam.constants';
import { IamModule } from './iam/iam.module';
import { KMSHandlers } from './kms/kms.constants';
import { KmsModule } from './kms/kms.module';
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 { SqsHandlers } from './sqs/sqs.constants';
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({
imports: [
@@ -23,12 +24,13 @@ import { SqsModule } from './sqs/sqs.module';
load: [localConfig],
isGlobal: true,
}),
IamModule,
PrismaModule,
AwsSharedEntitiesModule,
KmsModule,
SecretsManagerModule,
SnsModule,
SqsModule,
AwsSharedEntitiesModule,
StsModule,
],
controllers: [
AppController,
@@ -39,11 +41,11 @@ import { SqsModule } from './sqs/sqs.module';
provide: ActionHandlers,
useFactory: (...args) => args.reduce((m, hs) => ({ ...m, ...hs }), {}),
inject: [
KMSHandlers,
SecretsManagerHandlers,
SnsHandlers,
SqsHandlers,
SecretsManagerHandlers,
KMSHandlers,
IAMHandlers,
StsHandlers,
],
},
],

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 { Action } from '../action.enum';
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 { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
GrantTokens?: string[];
KeyId: string;
}
@@ -17,8 +16,6 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly kmsService: KmsService,
@InjectRepository(KmsKey)
private readonly keyRepo: Repository<KmsKey>,
) {
super();
}
@@ -27,6 +24,7 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
action = Action.KmsDescribeKey;
validator = Joi.object<QueryParams, true>({
KeyId: Joi.string().required(),
GrantTokens: Joi.array().items(Joi.string()),
});
protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties) {
@@ -38,16 +36,17 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
identifier: KeyId,
};
const [ type, pk ] = searchable.identifier.split('/');
const keyId: Promise<string> = type === 'key' ?
Promise.resolve(pk) :
this.kmsService.findKeyIdFromAlias(pk, searchable);
const keyId = await (type === 'key' ? Promise.resolve(pk) : this.kmsService.findKeyIdFromAlias(pk, searchable));
if (!keyId) {
throw new NotFoundException();
}
const keyRecord = await this.keyRepo.findOne({ where: {
id: await keyId,
region: searchable.region,
accountId: searchable.accountId,
}});
const keyRecord = await this.kmsService.findOneById(keyId);
if (!keyRecord) {
throw new NotFoundException();
}
return {
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' })
export class KmsKeyAlias extends BaseEntity {
// name: string;
// targetKeyId: string;
// accountId: string;
// region: string;
@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}`;
}
// 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 KeyUsage = 'SIGN_VERIFY' | 'ENCRYPT_DECRYPT' | 'GENERATE_VERIFY_MAC';
@Entity({ name: 'kms_key'})
export class KmsKey extends BaseEntity {
export class KmsKey implements PrismaKmsKey {
@PrimaryColumn()
id: string;
@Column({ name: 'usage' })
usage: KeyUsage;
@Column({ name: 'description' })
description: string;
@Column({ name: 'key_spec' })
keySpec: KeySpec;
@Column({ name: 'key' })
key: string;
@Column({ name: 'account_id', nullable: false })
accountId: string;
@Column({ name: 'region', nullable: false })
region: string;
createdAt: Date;
@CreateDateColumn()
createdAt: string;
constructor(p: PrismaKmsKey) {
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() {
return `arn:aws:kms:${this.region}:${this.accountId}:key/${this.id}`;

View File

@@ -1,22 +1,17 @@
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 { 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 { 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 = [
CreateAliasHandler,
DescribeKeyHandler,
GetPublicKeyHandler,
]
const actions = [
@@ -74,8 +69,8 @@ const actions = [
@Module({
imports: [
TypeOrmModule.forFeature([KmsKey, KmsKeyAlias]),
AwsSharedEntitiesModule,
PrismaModule,
],
providers: [
...handlers,

View File

@@ -1,22 +1,30 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../_prisma/prisma.service';
import { ArnParts } from '../util/breakdown-arn';
import { InjectRepository } from '@nestjs/typeorm';
import { KmsKeyAlias } from './kms-key-alias.entity';
import { Repository } from 'typeorm';
import { KmsKey } from './kms-key.entity';
@Injectable()
export class KmsService {
constructor(
@InjectRepository(KmsKeyAlias)
private readonly aliasRepo: Repository<KmsKeyAlias>,
private readonly prismaService: PrismaService,
) {}
async findKeyIdFromAlias(alias: string, arn: ArnParts): Promise<string> {
const record = await this.aliasRepo.findOne({ where: {
name: alias,
accountId: arn.accountId,
region: arn.region,
}});
return record.targetKeyId;
async findOneById(id: string): Promise<KmsKey | null> {
const pRecord = await this.prismaService.kmsKey.findFirst({
where: { id }
});
return pRecord ? new KmsKey(pRecord) : null;
}
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 { CommonConfig } from './config/common-config.interface';
import { AwsExceptionFilter } from './_context/exception.filter';
const bodyParser = require('body-parser');
(async () => {
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'}));
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 { PrismaService } from '../_prisma/prisma.service';
import { ArnUtil } from '../util/arn-util.static';
import { NotFoundException } from '../aws-shared-entities/aws-exceptions';
type QueryParams = {
SubscriptionArn: string;
@@ -34,7 +35,7 @@ export class UnsubscribeHandler extends AbstractActionHandler<QueryParams> {
const subscription = await this.prismaService.snsTopicSubscription.findFirst({ where: { id } });
if (!subscription) {
return;
throw new NotFoundException();
}
const arn = ArnUtil.fromTopicSub(subscription);

View File

@@ -8,7 +8,7 @@ import { SqsQueue } from './sqs-queue.entity';
type QueryParams = {
QueueUrl: string;
}
} & Record<string, string>;
@Injectable()
export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams> {
@@ -34,7 +34,7 @@ export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams
for (const header of Object.keys(params)) {
if (header.includes('DeleteMessageBatchRequestEntry') && header.includes('ReceiptHandle')) {
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) {
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
await this.sqsQueueEntryService.deleteMessage(accountId, name, ReceiptHandle);
await this.sqsQueueEntryService.deleteMessage(ReceiptHandle);
}
}

View File

@@ -1,5 +1,5 @@
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';
@@ -14,9 +14,9 @@ import { PurgeQueueHandler } from './purge-queue.handler';
import { ReceiveMessageHandler } from './receive-message.handler';
import { SetQueueAttributesHandler } from './set-queue-attributes.handler';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
import { SqsQueue } from './sqs-queue.entity';
import { SqsHandlers } from './sqs.constants';
import { DeleteMessageBatchHandler } from './delete-message-batch.handler';
import { PrismaModule } from '../_prisma/prisma.module';
const handlers = [
CreateQueueHandler,
@@ -55,8 +55,8 @@ const actions = [
@Module({
imports: [
TypeOrmModule.forFeature([SqsQueue]),
AwsSharedEntitiesModule,
PrismaModule,
],
providers: [
...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 = {
service: string;
region: string;
@@ -7,7 +9,7 @@ export type ArnParts = {
export const breakdownArn = (arn: string): ArnParts => {
if (!arn.startsWith('arn')) {
throw new Error('Invalid arn');
throw new InvalidArnException('Invalid arn');
}
const [_arn, _aws, service, region, accountId, ...identifierData] = arn.split(':');