diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a172bdd..b0e299f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,19 @@ model Audit { response String? } +model Secret { + versionId String @id + name String + description String? + secretString String + accountId String + region String + createdAt DateTime @default(now()) + deletionDate DateTime? + + @@index([name]) +} + model SnsTopic { name String @id accountId String diff --git a/src/secrets-manager/create-secret.dto.ts b/src/secrets-manager/create-secret.dto.ts deleted file mode 100644 index 48c1d95..0000000 --- a/src/secrets-manager/create-secret.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface CreateSecretDto { - versionId?: string; - name: string; - description?: string; - secretString?: string; - accountId: string; - region: string; -} diff --git a/src/secrets-manager/create-secret.handler.ts b/src/secrets-manager/create-secret.handler.ts index d126036..d7d7a58 100644 --- a/src/secrets-manager/create-secret.handler.ts +++ b/src/secrets-manager/create-secret.handler.ts @@ -1,7 +1,10 @@ import { Injectable } from '@nestjs/common'; +import { randomUUID } from 'crypto'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; +import { ArnUtil } from '../util/arn-util.static'; import { SecretService } from './secret.service'; type QueryParams = { @@ -22,7 +25,7 @@ export class CreateSecretHandler extends AbstractActionHandler { format = Format.Json; action = Action.SecretsManagerCreateSecret; - validator = Joi.object({ + validator = Joi.object({ Name: Joi.string().required(), Description: Joi.string().allow('', null), SecretString: Joi.string().allow('', null), @@ -34,7 +37,7 @@ export class CreateSecretHandler extends AbstractActionHandler { const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params; const secret = await this.secretService.create({ - versionId: ClientRequestToken, + versionId: ClientRequestToken ?? randomUUID(), description, name, secretString, @@ -42,6 +45,8 @@ export class CreateSecretHandler extends AbstractActionHandler { region: awsProperties.region, }); - return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name }; + const arn = ArnUtil.fromSecret(secret); + + return { ARN: arn, VersionId: secret.versionId, Name: secret.name }; } } diff --git a/src/secrets-manager/delete-secret.handler.ts b/src/secrets-manager/delete-secret.handler.ts index a9d263c..347fbcd 100644 --- a/src/secrets-manager/delete-secret.handler.ts +++ b/src/secrets-manager/delete-secret.handler.ts @@ -1,8 +1,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import * as Joi from 'joi'; + +import { PrismaService } from '../_prisma/prisma.service'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { Secret } from './secret.entity'; +import { ArnUtil } from '../util/arn-util.static'; import { SecretService } from './secret.service'; type QueryParams = { @@ -12,24 +14,25 @@ type QueryParams = { @Injectable() export class DeleteSecretHandler extends AbstractActionHandler { - + constructor( private readonly secretService: SecretService, + private readonly prismaService: PrismaService, ) { super(); } format = Format.Json; action = Action.SecretsManagerDeleteSecret; - validator = Joi.object({ + validator = Joi.object({ SecretId: Joi.string().required(), VersionId: Joi.string().allow(null, ''), }); - protected async handle({ SecretId, VersionId}: QueryParams, awsProperties: AwsProperties) { + protected async handle({ SecretId, VersionId }: QueryParams, awsProperties: AwsProperties) { - const name = Secret.getNameFromSecretId(SecretId); - const secret = VersionId ? + const name = ArnUtil.getSecretNameFromSecretId(SecretId); + const secret = VersionId ? await this.secretService.findByNameAndVersion(name, VersionId) : await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); @@ -37,10 +40,20 @@ export class DeleteSecretHandler extends AbstractActionHandler { throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); } - secret.deletionDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 5).toISOString(); - await secret.save(); + await this.prismaService.secret.update({ + data: { + deletionDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 5), + }, + where: { + versionId: secret.versionId, + name: secret.name, + } + }); + + const arn = ArnUtil.fromSecret(secret); + return { - Arn: secret.arn, + Arn: arn, DeletionDate: secret.deletionDate, Name: secret.name, } diff --git a/src/secrets-manager/describe-secret.handler.ts b/src/secrets-manager/describe-secret.handler.ts index 9ea78f6..d95c108 100644 --- a/src/secrets-manager/describe-secret.handler.ts +++ b/src/secrets-manager/describe-secret.handler.ts @@ -1,11 +1,11 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { Secret } from './secret.entity'; import { TagsService } from '../aws-shared-entities/tags.service'; +import { ArnUtil } from '../util/arn-util.static'; +import { SecretService } from './secret.service'; type QueryParams = { SecretId: string; @@ -13,10 +13,9 @@ type QueryParams = { @Injectable() export class DescribeSecretHandler extends AbstractActionHandler { - + constructor( - @InjectRepository(Secret) - private readonly secretRepo: Repository, + private readonly secretService: SecretService, private readonly tagsService: TagsService, ) { super(); @@ -28,20 +27,19 @@ 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' } }); + const name = ArnUtil.getSecretNameFromSecretId(SecretId); + const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); if (!secret) { throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); } - const tags = await this.tagsService.getByArn(secret.arn); + const arn = ArnUtil.fromSecret(secret); + const tags = await this.tagsService.getByArn(arn); const listOfTagPairs = TagsService.getJsonSafeTagsMap(tags); return { - "ARN": secret.arn, + "ARN": arn, "CreatedDate": new Date(secret.createdAt).toISOString(), "DeletedDate": secret.deletionDate ? new Date(secret.deletionDate).toISOString() : null, "Description": secret.description, diff --git a/src/secrets-manager/get-resource-policy.handler.ts b/src/secrets-manager/get-resource-policy.handler.ts index 27158f9..8938d2c 100644 --- a/src/secrets-manager/get-resource-policy.handler.ts +++ b/src/secrets-manager/get-resource-policy.handler.ts @@ -1,11 +1,11 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { Secret } from './secret.entity'; import { AttributesService } from '../aws-shared-entities/attributes.service'; +import { ArnUtil } from '../util/arn-util.static'; +import { SecretService } from './secret.service'; type QueryParams = { SecretId: string; @@ -15,8 +15,7 @@ type QueryParams = { export class GetResourcePolicyHandler extends AbstractActionHandler { constructor( - @InjectRepository(Secret) - private readonly secretRepo: Repository, + private readonly secretService: SecretService, private readonly attributesService: AttributesService, ) { super(); @@ -28,16 +27,17 @@ export class GetResourcePolicyHandler extends AbstractActionHandler { protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) { - const name = Secret.getNameFromSecretId(SecretId); - const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } }); + const name = ArnUtil.getSecretNameFromSecretId(SecretId); + const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); if (!secret) { throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); } - const attribute = await this.attributesService.getResourcePolicyByArn(secret.arn); + const arn = ArnUtil.fromSecret(secret); + const attribute = await this.attributesService.getResourcePolicyByArn(arn); return { - ARN: secret.arn, + ARN: arn, Name: secret.name, ResourcePolicy: attribute?.value, } diff --git a/src/secrets-manager/get-secret-value.handler.ts b/src/secrets-manager/get-secret-value.handler.ts index cdc3119..ef6cd4d 100644 --- a/src/secrets-manager/get-secret-value.handler.ts +++ b/src/secrets-manager/get-secret-value.handler.ts @@ -1,8 +1,9 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { Secret } from './secret.entity'; +import { ArnUtil } from '../util/arn-util.static'; import { SecretService } from './secret.service'; type QueryParams = { @@ -28,7 +29,7 @@ export class GetSecretValueHandler extends AbstractActionHandler { protected async handle({ SecretId, VersionId}: QueryParams, awsProperties: AwsProperties) { - const name = Secret.getNameFromSecretId(SecretId); + const name = ArnUtil.getSecretNameFromSecretId(SecretId); const secret = VersionId ? await this.secretService.findByNameAndVersion(name, VersionId) : await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); @@ -37,8 +38,10 @@ export class GetSecretValueHandler extends AbstractActionHandler { throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); } + const arn = ArnUtil.fromSecret(secret); + return { - ARN: secret.arn, + ARN: arn, CreatedDate: new Date(secret.createdAt).valueOf() / 1000, Name: secret.name, SecretString: secret.secretString, diff --git a/src/secrets-manager/put-resource-policy.handler.ts b/src/secrets-manager/put-resource-policy.handler.ts index e1f4cca..0761c55 100644 --- a/src/secrets-manager/put-resource-policy.handler.ts +++ b/src/secrets-manager/put-resource-policy.handler.ts @@ -1,11 +1,11 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { Secret } from './secret.entity'; import { AttributesService } from '../aws-shared-entities/attributes.service'; +import { ArnUtil } from '../util/arn-util.static'; +import { SecretService } from './secret.service'; type QueryParams = { SecretId: string; @@ -16,8 +16,7 @@ type QueryParams = { export class PutResourcePolicyHandler extends AbstractActionHandler { constructor( - @InjectRepository(Secret) - private readonly secretRepo: Repository, + private readonly secretService: SecretService, private readonly attributesService: AttributesService, ) { super(); @@ -32,16 +31,17 @@ export class PutResourcePolicyHandler extends AbstractActionHandler { protected async handle({ SecretId, ResourcePolicy }: QueryParams, awsProperties: AwsProperties) { - const name = Secret.getNameFromSecretId(SecretId); - const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } }); + const name = ArnUtil.getSecretNameFromSecretId(SecretId); + const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); if (!secret) { throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); } - await this.attributesService.createResourcePolicy(secret.arn, ResourcePolicy); + const arn = ArnUtil.fromSecret(secret); + await this.attributesService.createResourcePolicy(arn, ResourcePolicy); return { - ARN: secret.arn, + ARN: arn, Name: secret.name, } } diff --git a/src/secrets-manager/put-secret-value.handler.ts b/src/secrets-manager/put-secret-value.handler.ts index 704d895..329f1ea 100644 --- a/src/secrets-manager/put-secret-value.handler.ts +++ b/src/secrets-manager/put-secret-value.handler.ts @@ -1,8 +1,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import { randomUUID } from 'crypto'; +import * as Joi from 'joi'; + import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; -import * as Joi from 'joi'; -import { Secret } from './secret.entity'; +import { ArnUtil } from '../util/arn-util.static'; import { SecretService } from './secret.service'; type QueryParams = { @@ -31,7 +33,7 @@ export class PutSecretValueHandler extends AbstractActionHandler { protected async handle(params: QueryParams, awsProperties: AwsProperties) { const { SecretId, SecretString: secretString, ClientRequestToken } = params; - const name = Secret.getNameFromSecretId(SecretId); + const name = ArnUtil.getSecretNameFromSecretId(SecretId); const oldSecret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); if (!oldSecret) { @@ -39,13 +41,15 @@ export class PutSecretValueHandler extends AbstractActionHandler { } const secret = await this.secretService.create({ - versionId: ClientRequestToken, + versionId: ClientRequestToken ?? randomUUID(), name: oldSecret.name, secretString, accountId: awsProperties.accountId, region: awsProperties.region, }); - return { ARN: secret.arn, VersionId: secret.versionId, Name: secret.name, VersionStages: [] } + const arn = ArnUtil.fromSecret(secret); + + return { ARN: arn, VersionId: secret.versionId, Name: secret.name, VersionStages: [] } } } diff --git a/src/secrets-manager/secret.entity.ts b/src/secrets-manager/secret.entity.ts deleted file mode 100644 index ddb27bc..0000000 --- a/src/secrets-manager/secret.entity.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { BaseEntity, Column, CreateDateColumn, Entity, Index, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity('secret') -export class Secret extends BaseEntity { - - @PrimaryColumn({ name: 'versionId' }) - versionId: string; - - @Column({ name: 'name', nullable: false }) - @Index() - name: string; - - @Column({ name: 'description', nullable: true }) - description: string; - - @Column({ name: 'secret_string', nullable: true }) - secretString: string; - - @Column({ name: 'account_id', nullable: false }) - accountId: string; - - @Column({ name: 'region', nullable: false }) - region: string; - - @CreateDateColumn() - createdAt: string; - - @Column({ name: 'deletion_date', nullable: true }) - deletionDate: string; - - get arn(): string { - return `arn:aws:secretsmanager:${this.region}:${this.accountId}:${this.name}`; - } - - static getNameFromSecretId(secretId: string) { - const parts = secretId.split(':'); - return parts.length > 1 ? parts.pop() : secretId; - } -} diff --git a/src/secrets-manager/secret.service.ts b/src/secrets-manager/secret.service.ts index 5a282a1..b8e4f0f 100644 --- a/src/secrets-manager/secret.service.ts +++ b/src/secrets-manager/secret.service.ts @@ -1,32 +1,30 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { CreateSecretDto } from './create-secret.dto'; -import { Secret } from './secret.entity'; -import * as uuid from 'uuid'; +import { Prisma, Secret } from '@prisma/client'; +import { randomUUID } from 'crypto'; + +import { PrismaService } from '../_prisma/prisma.service'; @Injectable() export class SecretService { constructor( - @InjectRepository(Secret) - private readonly secretRepo: Repository, + private readonly prismaService: PrismaService, ) {} - async findLatestByNameAndRegion(name: string, region: string): Promise { - return await this.secretRepo.findOne({ where: { name, region }, order: { createdAt: 'DESC' } }); + async findLatestByNameAndRegion(name: string, region: string): Promise { + return await this.prismaService.secret.findFirst({ where: { name, region }, orderBy: { createdAt: 'desc' } }); } - async findByNameAndVersion(name: string, versionId: string): Promise { - // TypeORM BUG: https://github.com/typeorm/typeorm/issues/5694 - Cannot use findOne here - const [ secret ] = await this.secretRepo.find({ where: { name, versionId } }); - return secret; + async findByNameAndVersion(name: string, versionId: string): Promise { + return await this.prismaService.secret.findFirst({ where: { name, versionId } }); } - async create(dto: CreateSecretDto): Promise { - return await this.secretRepo.create({ - ...dto, - versionId: dto.versionId ?? uuid.v4(), - }).save(); + async create(data: Prisma.SecretCreateInput): Promise { + return await this.prismaService.secret.create({ + data: { + ...data, + versionId: data.versionId ?? randomUUID(), + } + }); } } diff --git a/src/secrets-manager/secrets-manager.module.ts b/src/secrets-manager/secrets-manager.module.ts index c23cb2d..31acbe1 100644 --- a/src/secrets-manager/secrets-manager.module.ts +++ b/src/secrets-manager/secrets-manager.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; + +import { PrismaModule } from '../_prisma/prisma.module'; import { Format } from '../abstract-action.handler'; import { Action } from '../action.enum'; import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module'; @@ -12,7 +13,6 @@ import { GetResourcePolicyHandler } from './get-resource-policy.handler'; import { GetSecretValueHandler } from './get-secret-value.handler'; import { PutResourcePolicyHandler } from './put-resource-policy.handler'; import { PutSecretValueHandler } from './put-secret-value.handler'; -import { Secret } from './secret.entity'; import { SecretService } from './secret.service'; import { SecretsManagerHandlers } from './secrets-manager.constants'; @@ -53,7 +53,7 @@ const actions = [ @Module({ imports: [ - TypeOrmModule.forFeature([Secret]), + PrismaModule, AwsSharedEntitiesModule, ], providers: [ diff --git a/src/util/arn-util.static.ts b/src/util/arn-util.static.ts index c7ab1f0..4f6adb2 100644 --- a/src/util/arn-util.static.ts +++ b/src/util/arn-util.static.ts @@ -1,4 +1,4 @@ -import { SnsTopic, SnsTopicSubscription } from "@prisma/client"; +import { Secret, SnsTopic, SnsTopicSubscription } from "@prisma/client"; export class ArnUtil { @@ -9,4 +9,13 @@ export class ArnUtil { static fromTopicSub(topicSub: SnsTopicSubscription): string { return `${topicSub.topicArn}:${topicSub.id}`; } + + static fromSecret(secret: Secret): string { + return `arn:aws:secretsmanager:${secret.region}:${secret.accountId}:${secret.name}`; + } + + static getSecretNameFromSecretId(secretId: string): string { + const parts = secretId.split(':'); + return parts.length > 1 ? parts.pop() as string : secretId; + } }