diff --git a/.drone.yml b/.drone.yml index b405e23..eec3da9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 diff --git a/src/app.controller.ts b/src/app.controller.ts index 27486c9..ed3a609 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -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 = { diff --git a/src/iam/create-policy.handler.ts b/src/iam/create-policy.handler.ts index 66462ff..4b187e1 100644 --- a/src/iam/create-policy.handler.ts +++ b/src/iam/create-policy.handler.ts @@ -41,7 +41,7 @@ export class CreatePolicyHandler extends AbstractActionHandler { return { Policy: { PolicyName: policy.name, - DefaultVersionId: 'v1', + DefaultVersionId: policy.version, PolicyId: policy.id, Path: '/', Arn: policy.arn, diff --git a/src/iam/create-role.handler.ts b/src/iam/create-role.handler.ts index 86ff38f..8dc9190 100644 --- a/src/iam/create-role.handler.ts +++ b/src/iam/create-role.handler.ts @@ -12,6 +12,7 @@ type QueryParams = { RoleName: string; Path: string; AssumeRolePolicyDocument: string; + MaxSessionDuration: number; } @Injectable() @@ -32,9 +33,10 @@ export class CreateRoleHandler extends AbstractActionHandler { 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 { path: Path, accountId: awsProperties.accountId, assumeRolePolicyDocumentId: policy.id, + maxSessionDuration: MaxSessionDuration, }).save(); const role = await this.roleRepo.findOne({ where: { id }}); diff --git a/src/iam/get-policy-version.handler.ts b/src/iam/get-policy-version.handler.ts new file mode 100644 index 0000000..981a8c8 --- /dev/null +++ b/src/iam/get-policy-version.handler.ts @@ -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 { + + constructor( + @InjectRepository(IamPolicy) + private readonly policyRepo: Repository, + @InjectRepository(IamRolePolicyAttachment) + private readonly attachmentRepo: Repository, + ) { + super(); + } + + format = Format.Xml; + action = Action.IamGetPolicyVersion; + validator = Joi.object({ + 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(), + } + } + } +} diff --git a/src/iam/get-policy.handler.ts b/src/iam/get-policy.handler.ts index 208d679..82e4d1c 100644 --- a/src/iam/get-policy.handler.ts +++ b/src/iam/get-policy.handler.ts @@ -45,7 +45,7 @@ export class GetPolicyHandler extends AbstractActionHandler { return { Policy: { PolicyName: policy.name, - DefaultVersionId: `v${policy.version}`, + DefaultVersionId: policy.version, PolicyId: policy.id, Path: '/', Arn: policy.arn, diff --git a/src/iam/iam-role.entity.ts b/src/iam/iam-role.entity.ts index 27eee9e..2676fd0 100644 --- a/src/iam/iam-role.entity.ts +++ b/src/iam/iam-role.entity.ts @@ -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, } } } diff --git a/src/iam/iam.module.ts b/src/iam/iam.module.ts index 48e7026..40c8794 100644 --- a/src/iam/iam.module.ts +++ b/src/iam/iam.module.ts @@ -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, ] diff --git a/src/kms/describe-key.handler.ts b/src/kms/describe-key.handler.ts index 2445516..b34d722 100644 --- a/src/kms/describe-key.handler.ts +++ b/src/kms/describe-key.handler.ts @@ -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 { constructor( - @InjectRepository(KmsKeyAlias) - private readonly aliasRepo: Repository, + private readonly kmsService: KmsService, @InjectRepository(KmsKey) private readonly keyRepo: Repository, ) { @@ -41,7 +40,7 @@ export class DescribeKeyHandler extends AbstractActionHandler { const [ type, pk ] = searchable.identifier.split('/'); const keyId: Promise = 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 { KeyMetadata: keyRecord.metadata, } } - - private async findKeyIdFromAlias(alias: string ,arn: ArnParts): Promise { - const record = await this.aliasRepo.findOne({ where: { - name: alias, - accountId: arn.accountId, - region: arn.region, - }}); - return record.targetKeyId; - } } diff --git a/src/kms/get-public-key.handler.ts b/src/kms/get-public-key.handler.ts new file mode 100644 index 0000000..e093881 --- /dev/null +++ b/src/kms/get-public-key.handler.ts @@ -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 { + + constructor( + + @InjectRepository(KmsKey) + private readonly keyRepo: Repository, + private readonly kmsService: KmsService, + ) { + super(); + } + + format = Format.Json; + action = Action.KmsGetPublicKey; + validator = Joi.object({ + KeyId: Joi.string().required(), + GrantTokens: Joi.array().items(Joi.string()), + }); + + protected async handle({ KeyId }: QueryParams, awsProperties: AwsProperties): Promise { + + 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 = 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'), + } + } +} diff --git a/src/kms/kms-key.entity.ts b/src/kms/kms-key.entity.ts index 834adf0..0ef2613 100644 --- a/src/kms/kms-key.entity.ts +++ b/src/kms/kms-key.entity.ts @@ -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; diff --git a/src/kms/kms.module.ts b/src/kms/kms.module.ts index e52fe3c..74dbf75 100644 --- a/src/kms/kms.module.ts +++ b/src/kms/kms.module.ts @@ -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), ], diff --git a/src/kms/kms.service.ts b/src/kms/kms.service.ts new file mode 100644 index 0000000..d5abbb1 --- /dev/null +++ b/src/kms/kms.service.ts @@ -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, + ) {} + + async findKeyIdFromAlias(alias: string, arn: ArnParts): Promise { + const record = await this.aliasRepo.findOne({ where: { + name: alias, + accountId: arn.accountId, + region: arn.region, + }}); + return record.targetKeyId; + } +} diff --git a/src/secrets-manager/describe-secret.handler.ts b/src/secrets-manager/describe-secret.handler.ts index c6daa02..9ea78f6 100644 --- a/src/secrets-manager/describe-secret.handler.ts +++ b/src/secrets-manager/describe-secret.handler.ts @@ -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' } }); diff --git a/src/sqs/delete-message-batch.handler.ts b/src/sqs/delete-message-batch.handler.ts new file mode 100644 index 0000000..bcc95d6 --- /dev/null +++ b/src/sqs/delete-message-batch.handler.ts @@ -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 { + + constructor( + private readonly sqsQueueEntryService: SqsQueueEntryService, + ) { + super(); + } + + audit = false; + format = Format.Xml; + action = Action.SqsDeleteMessageBatch; + validator = Joi.object({ + 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); + } + } + } +} diff --git a/src/sqs/sqs.module.ts b/src/sqs/sqs.module.ts index 4502f18..980c9e1 100644 --- a/src/sqs/sqs.module.ts +++ b/src/sqs/sqs.module.ts @@ -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,