General fixes and new kms support

This commit is contained in:
Matthew Bessette 2023-06-27 15:03:04 -04:00
parent e1aaeaa90e
commit a5c90f7a26
16 changed files with 278 additions and 29 deletions

View File

@ -2,19 +2,19 @@ kind: pipeline
type: docker
name: default
steps:
- name: build backend
- name: build
image: docker.thiccdata.io/nvm-node-debian:latest
commands:
- docker build -t docker.thiccdata.io/mtg-event-manager-backend:latest -f ./backend/Dockerfile ./
- docker build -t docker.thiccdata.io/mtg-event-manager-backend:latest ./
- docker push docker.thiccdata.io/mtg-event-manager-backend:latest
volumes:
- name: docker
path: /var/run/docker.sock
- name: docker
path: /var/run/docker.sock
trigger:
branch:
- main
- main
volumes:
- name: docker
host:
path: /var/run/docker.sock
- name: docker
host:
path: /var/run/docker.sock

View File

@ -39,7 +39,7 @@ export class AppController {
}).validate(queryParams, { allowUnknown: true });
if (actionError) {
throw new BadRequestException(actionError);
throw new BadRequestException(actionError.message, { cause: actionError });
}
const action = queryParams[actionKey];
@ -47,7 +47,7 @@ export class AppController {
const { error: validatorError, value: validQueryParams } = handler.validator.validate(queryParams, { allowUnknown: true, abortEarly: false });
if (validatorError) {
throw new BadRequestException(validatorError);
throw new BadRequestException(validatorError.message, { cause: validatorError });
}
const awsProperties = {

View File

@ -41,7 +41,7 @@ export class CreatePolicyHandler extends AbstractActionHandler<QueryParams> {
return {
Policy: {
PolicyName: policy.name,
DefaultVersionId: 'v1',
DefaultVersionId: policy.version,
PolicyId: policy.id,
Path: '/',
Arn: policy.arn,

View File

@ -12,6 +12,7 @@ type QueryParams = {
RoleName: string;
Path: string;
AssumeRolePolicyDocument: string;
MaxSessionDuration: number;
}
@Injectable()
@ -32,9 +33,10 @@ export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
RoleName: Joi.string().required(),
Path: Joi.string().required(),
AssumeRolePolicyDocument: Joi.string().required(),
MaxSessionDuration: Joi.number().default(3600),
});
protected async handle({ RoleName, Path, AssumeRolePolicyDocument }: QueryParams, awsProperties: AwsProperties) {
protected async handle({ RoleName, Path, AssumeRolePolicyDocument, MaxSessionDuration }: QueryParams, awsProperties: AwsProperties) {
const policy = await this.policyRepo.create({
id: uuid.v4(),
@ -51,6 +53,7 @@ export class CreateRoleHandler extends AbstractActionHandler<QueryParams> {
path: Path,
accountId: awsProperties.accountId,
assumeRolePolicyDocumentId: policy.id,
maxSessionDuration: MaxSessionDuration,
}).save();
const role = await this.roleRepo.findOne({ where: { id }});

View File

@ -0,0 +1,54 @@
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

@ -45,7 +45,7 @@ export class GetPolicyHandler extends AbstractActionHandler<QueryParams> {
return {
Policy: {
PolicyName: policy.name,
DefaultVersionId: `v${policy.version}`,
DefaultVersionId: policy.version,
PolicyId: policy.id,
Path: '/',
Arn: policy.arn,

View File

@ -19,6 +19,9 @@ export class IamRole extends BaseEntity {
@Column({ name: 'account_id', nullable: false })
accountId: string;
@Column({ name: 'max_session_duration', nullable: false, default: 0 })
maxSessionDuration: number;
@CreateDateColumn()
createdAt: string;
@ -43,6 +46,7 @@ export class IamRole extends BaseEntity {
AssumeRolePolicyDocument: this.assumeRolePolicyDocument.document,
CreateDate: new Date(this.createdAt).toISOString(),
RoleId: this.id,
MaxSessionDuration: this.maxSessionDuration,
}
}
}

View File

@ -9,6 +9,7 @@ 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';
@ -25,6 +26,7 @@ const handlers = [
CreateRoleHandler,
GetPolicyHandler,
GetRoleHandler,
GetPolicyVersionHandler,
ListAttachedRolePoliciesHandler,
ListRolePoliciesHandler,
]

View File

@ -2,11 +2,11 @@ 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';
import { KmsKey } from './kms-key.entity';
import { ArnParts, breakdownArn } from '../util/breakdown-arn';
import { breakdownArn } from '../util/breakdown-arn';
import { KmsService } from './kms.service';
type QueryParams = {
KeyId: string;
@ -16,8 +16,7 @@ type QueryParams = {
export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
constructor(
@InjectRepository(KmsKeyAlias)
private readonly aliasRepo: Repository<KmsKeyAlias>,
private readonly kmsService: KmsService,
@InjectRepository(KmsKey)
private readonly keyRepo: Repository<KmsKey>,
) {
@ -41,7 +40,7 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
const [ type, pk ] = searchable.identifier.split('/');
const keyId: Promise<string> = type === 'key' ?
Promise.resolve(pk) :
this.findKeyIdFromAlias(pk, searchable);
this.kmsService.findKeyIdFromAlias(pk, searchable);
const keyRecord = await this.keyRepo.findOne({ where: {
@ -54,13 +53,4 @@ export class DescribeKeyHandler extends AbstractActionHandler<QueryParams> {
KeyMetadata: keyRecord.metadata,
}
}
private 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;
}
}

View File

@ -0,0 +1,123 @@
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,5 +1,8 @@
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm';
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 {
@ -7,13 +10,13 @@ export class KmsKey extends BaseEntity {
id: string;
@Column({ name: 'usage' })
usage: string;
usage: KeyUsage;
@Column({ name: 'description' })
description: string;
@Column({ name: 'key_spec' })
keySpec: string;
keySpec: KeySpec;
@Column({ name: 'key' })
key: string;

View File

@ -10,10 +10,13 @@ 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';
const handlers = [
CreateAliasHandler,
DescribeKeyHandler,
GetPublicKeyHandler,
]
const actions = [
@ -76,6 +79,7 @@ const actions = [
],
providers: [
...handlers,
KmsService,
ExistingActionHandlersProvider(handlers),
DefaultActionHandlerProvider(KMSHandlers, Format.Json, actions),
],

22
src/kms/kms.service.ts Normal file
View File

@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { ArnParts } from '../util/breakdown-arn';
import { InjectRepository } from '@nestjs/typeorm';
import { KmsKeyAlias } from './kms-key-alias.entity';
import { Repository } from 'typeorm';
@Injectable()
export class KmsService {
constructor(
@InjectRepository(KmsKeyAlias)
private readonly aliasRepo: Repository<KmsKeyAlias>,
) {}
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;
}
}

View File

@ -28,6 +28,8 @@ export class DescribeSecretHandler extends AbstractActionHandler {
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
console.log({ SecretId })
const name = Secret.getNameFromSecretId(SecretId);
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum';
import * as Joi from 'joi';
import { SqsQueue } from './sqs-queue.entity';
import { SqsQueueEntryService } from './sqs-queue-entry.service';
type QueryParams = {
QueueUrl: string;
}
@Injectable()
export class DeleteMessageBatchHandler extends AbstractActionHandler<QueryParams> {
constructor(
private readonly sqsQueueEntryService: SqsQueueEntryService,
) {
super();
}
audit = false;
format = Format.Xml;
action = Action.SqsDeleteMessageBatch;
validator = Joi.object<QueryParams>({
QueueUrl: Joi.string().required(),
});
protected async handle( params : QueryParams, awsProperties: AwsProperties) {
const { QueueUrl } = params;
const [accountId, name] = SqsQueue.tryGetAccountIdAndNameFromPathOrArn(QueueUrl);
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);
}
}
}
}

View File

@ -16,9 +16,11 @@ 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';
const handlers = [
CreateQueueHandler,
DeleteMessageBatchHandler,
DeleteMessageHandler,
DeleteQueueHandler,
GetQueueAttributesHandler,