Migrates secrets to prisma

This commit is contained in:
Matthew Bessette 2024-12-18 19:31:43 -05:00
parent a7fdedd310
commit 22da8d73d3
13 changed files with 121 additions and 125 deletions

View File

@ -24,6 +24,19 @@ model Audit {
response String? 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 { model SnsTopic {
name String @id name String @id
accountId String accountId String

View File

@ -1,8 +0,0 @@
export interface CreateSecretDto {
versionId?: string;
name: string;
description?: string;
secretString?: string;
accountId: string;
region: string;
}

View File

@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { randomUUID } from 'crypto';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
type QueryParams = { type QueryParams = {
@ -22,7 +25,7 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
format = Format.Json; format = Format.Json;
action = Action.SecretsManagerCreateSecret; action = Action.SecretsManagerCreateSecret;
validator = Joi.object<QueryParams, true>({ validator = Joi.object<QueryParams, true>({
Name: Joi.string().required(), Name: Joi.string().required(),
Description: Joi.string().allow('', null), Description: Joi.string().allow('', null),
SecretString: Joi.string().allow('', null), SecretString: Joi.string().allow('', null),
@ -34,7 +37,7 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params; const { Name: name, Description: description, SecretString: secretString, ClientRequestToken } = params;
const secret = await this.secretService.create({ const secret = await this.secretService.create({
versionId: ClientRequestToken, versionId: ClientRequestToken ?? randomUUID(),
description, description,
name, name,
secretString, secretString,
@ -42,6 +45,8 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
region: awsProperties.region, 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 };
} }
} }

View File

@ -1,8 +1,10 @@
import { BadRequestException, Injectable } from '@nestjs/common'; 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 { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import { ArnUtil } from '../util/arn-util.static';
import { Secret } from './secret.entity';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
type QueryParams = { type QueryParams = {
@ -12,24 +14,25 @@ type QueryParams = {
@Injectable() @Injectable()
export class DeleteSecretHandler extends AbstractActionHandler { export class DeleteSecretHandler extends AbstractActionHandler {
constructor( constructor(
private readonly secretService: SecretService, private readonly secretService: SecretService,
private readonly prismaService: PrismaService,
) { ) {
super(); super();
} }
format = Format.Json; format = Format.Json;
action = Action.SecretsManagerDeleteSecret; action = Action.SecretsManagerDeleteSecret;
validator = Joi.object<QueryParams, true>({ validator = Joi.object<QueryParams, true>({
SecretId: Joi.string().required(), SecretId: Joi.string().required(),
VersionId: Joi.string().allow(null, ''), 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 name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = VersionId ? const secret = VersionId ?
await this.secretService.findByNameAndVersion(name, VersionId) : await this.secretService.findByNameAndVersion(name, VersionId) :
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); 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."); 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 this.prismaService.secret.update({
await secret.save(); data: {
deletionDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 5),
},
where: {
versionId: secret.versionId,
name: secret.name,
}
});
const arn = ArnUtil.fromSecret(secret);
return { return {
Arn: secret.arn, Arn: arn,
DeletionDate: secret.deletionDate, DeletionDate: secret.deletionDate,
Name: secret.name, Name: secret.name,
} }

View File

@ -1,11 +1,11 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import * as Joi from 'joi';
import { Repository } from 'typeorm';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi';
import { Secret } from './secret.entity';
import { TagsService } from '../aws-shared-entities/tags.service'; import { TagsService } from '../aws-shared-entities/tags.service';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -13,10 +13,9 @@ type QueryParams = {
@Injectable() @Injectable()
export class DescribeSecretHandler extends AbstractActionHandler { export class DescribeSecretHandler extends AbstractActionHandler {
constructor( constructor(
@InjectRepository(Secret) private readonly secretService: SecretService,
private readonly secretRepo: Repository<Secret>,
private readonly tagsService: TagsService, private readonly tagsService: TagsService,
) { ) {
super(); super();
@ -28,20 +27,19 @@ export class DescribeSecretHandler extends AbstractActionHandler {
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) { protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
console.log({ SecretId }) const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
const name = Secret.getNameFromSecretId(SecretId);
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } });
if (!secret) { if (!secret) {
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); 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); const listOfTagPairs = TagsService.getJsonSafeTagsMap(tags);
return { return {
"ARN": secret.arn, "ARN": arn,
"CreatedDate": new Date(secret.createdAt).toISOString(), "CreatedDate": new Date(secret.createdAt).toISOString(),
"DeletedDate": secret.deletionDate ? new Date(secret.deletionDate).toISOString() : null, "DeletedDate": secret.deletionDate ? new Date(secret.deletionDate).toISOString() : null,
"Description": secret.description, "Description": secret.description,

View File

@ -1,11 +1,11 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import * as Joi from 'joi';
import { Repository } from 'typeorm';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi';
import { Secret } from './secret.entity';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -15,8 +15,7 @@ type QueryParams = {
export class GetResourcePolicyHandler extends AbstractActionHandler { export class GetResourcePolicyHandler extends AbstractActionHandler {
constructor( constructor(
@InjectRepository(Secret) private readonly secretService: SecretService,
private readonly secretRepo: Repository<Secret>,
private readonly attributesService: AttributesService, private readonly attributesService: AttributesService,
) { ) {
super(); super();
@ -28,16 +27,17 @@ export class GetResourcePolicyHandler extends AbstractActionHandler {
protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) { protected async handle({ SecretId }: QueryParams, awsProperties: AwsProperties) {
const name = Secret.getNameFromSecretId(SecretId); const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } }); const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
if (!secret) { if (!secret) {
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); 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 { return {
ARN: secret.arn, ARN: arn,
Name: secret.name, Name: secret.name,
ResourcePolicy: attribute?.value, ResourcePolicy: attribute?.value,
} }

View File

@ -1,8 +1,9 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import { ArnUtil } from '../util/arn-util.static';
import { Secret } from './secret.entity';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
type QueryParams = { type QueryParams = {
@ -28,7 +29,7 @@ export class GetSecretValueHandler extends AbstractActionHandler {
protected async handle({ SecretId, VersionId}: QueryParams, awsProperties: AwsProperties) { protected async handle({ SecretId, VersionId}: QueryParams, awsProperties: AwsProperties) {
const name = Secret.getNameFromSecretId(SecretId); const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = VersionId ? const secret = VersionId ?
await this.secretService.findByNameAndVersion(name, VersionId) : await this.secretService.findByNameAndVersion(name, VersionId) :
await this.secretService.findLatestByNameAndRegion(name, awsProperties.region); 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."); throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for.");
} }
const arn = ArnUtil.fromSecret(secret);
return { return {
ARN: secret.arn, ARN: arn,
CreatedDate: new Date(secret.createdAt).valueOf() / 1000, CreatedDate: new Date(secret.createdAt).valueOf() / 1000,
Name: secret.name, Name: secret.name,
SecretString: secret.secretString, SecretString: secret.secretString,

View File

@ -1,11 +1,11 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import * as Joi from 'joi';
import { Repository } from 'typeorm';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi';
import { Secret } from './secret.entity';
import { AttributesService } from '../aws-shared-entities/attributes.service'; import { AttributesService } from '../aws-shared-entities/attributes.service';
import { ArnUtil } from '../util/arn-util.static';
import { SecretService } from './secret.service';
type QueryParams = { type QueryParams = {
SecretId: string; SecretId: string;
@ -16,8 +16,7 @@ type QueryParams = {
export class PutResourcePolicyHandler extends AbstractActionHandler { export class PutResourcePolicyHandler extends AbstractActionHandler {
constructor( constructor(
@InjectRepository(Secret) private readonly secretService: SecretService,
private readonly secretRepo: Repository<Secret>,
private readonly attributesService: AttributesService, private readonly attributesService: AttributesService,
) { ) {
super(); super();
@ -32,16 +31,17 @@ export class PutResourcePolicyHandler extends AbstractActionHandler {
protected async handle({ SecretId, ResourcePolicy }: QueryParams, awsProperties: AwsProperties) { protected async handle({ SecretId, ResourcePolicy }: QueryParams, awsProperties: AwsProperties) {
const name = Secret.getNameFromSecretId(SecretId); const name = ArnUtil.getSecretNameFromSecretId(SecretId);
const secret = await this.secretRepo.findOne({ where: { name }, order: { createdAt: 'DESC' } }); const secret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
if (!secret) { if (!secret) {
throw new BadRequestException('ResourceNotFoundException', "Secrets Manager can't find the resource that you asked for."); 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 { return {
ARN: secret.arn, ARN: arn,
Name: secret.name, Name: secret.name,
} }
} }

View File

@ -1,8 +1,10 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { randomUUID } from 'crypto';
import * as Joi from 'joi';
import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler'; import { AbstractActionHandler, AwsProperties, Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import * as Joi from 'joi'; import { ArnUtil } from '../util/arn-util.static';
import { Secret } from './secret.entity';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
type QueryParams = { type QueryParams = {
@ -31,7 +33,7 @@ export class PutSecretValueHandler extends AbstractActionHandler<QueryParams> {
protected async handle(params: QueryParams, awsProperties: AwsProperties) { protected async handle(params: QueryParams, awsProperties: AwsProperties) {
const { SecretId, SecretString: secretString, ClientRequestToken } = params; 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); const oldSecret = await this.secretService.findLatestByNameAndRegion(name, awsProperties.region);
if (!oldSecret) { if (!oldSecret) {
@ -39,13 +41,15 @@ export class PutSecretValueHandler extends AbstractActionHandler<QueryParams> {
} }
const secret = await this.secretService.create({ const secret = await this.secretService.create({
versionId: ClientRequestToken, versionId: ClientRequestToken ?? randomUUID(),
name: oldSecret.name, name: oldSecret.name,
secretString, secretString,
accountId: awsProperties.accountId, accountId: awsProperties.accountId,
region: awsProperties.region, 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: [] }
} }
} }

View File

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

View File

@ -1,32 +1,30 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { Prisma, Secret } from '@prisma/client';
import { Repository } from 'typeorm'; import { randomUUID } from 'crypto';
import { CreateSecretDto } from './create-secret.dto';
import { Secret } from './secret.entity'; import { PrismaService } from '../_prisma/prisma.service';
import * as uuid from 'uuid';
@Injectable() @Injectable()
export class SecretService { export class SecretService {
constructor( constructor(
@InjectRepository(Secret) private readonly prismaService: PrismaService,
private readonly secretRepo: Repository<Secret>,
) {} ) {}
async findLatestByNameAndRegion(name: string, region: string): Promise<Secret> { async findLatestByNameAndRegion(name: string, region: string): Promise<Secret | null> {
return await this.secretRepo.findOne({ where: { name, region }, order: { createdAt: 'DESC' } }); return await this.prismaService.secret.findFirst({ where: { name, region }, orderBy: { createdAt: 'desc' } });
} }
async findByNameAndVersion(name: string, versionId: string): Promise<Secret> { async findByNameAndVersion(name: string, versionId: string): Promise<Secret | null> {
// TypeORM BUG: https://github.com/typeorm/typeorm/issues/5694 - Cannot use findOne here return await this.prismaService.secret.findFirst({ where: { name, versionId } });
const [ secret ] = await this.secretRepo.find({ where: { name, versionId } });
return secret;
} }
async create(dto: CreateSecretDto): Promise<Secret> { async create(data: Prisma.SecretCreateInput): Promise<Secret> {
return await this.secretRepo.create({ return await this.prismaService.secret.create({
...dto, data: {
versionId: dto.versionId ?? uuid.v4(), ...data,
}).save(); versionId: data.versionId ?? randomUUID(),
}
});
} }
} }

View File

@ -1,5 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PrismaModule } from '../_prisma/prisma.module';
import { Format } from '../abstract-action.handler'; import { Format } from '../abstract-action.handler';
import { Action } from '../action.enum'; import { Action } from '../action.enum';
import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module'; import { AwsSharedEntitiesModule } from '../aws-shared-entities/aws-shared-entities.module';
@ -12,7 +13,6 @@ import { GetResourcePolicyHandler } from './get-resource-policy.handler';
import { GetSecretValueHandler } from './get-secret-value.handler'; import { GetSecretValueHandler } from './get-secret-value.handler';
import { PutResourcePolicyHandler } from './put-resource-policy.handler'; import { PutResourcePolicyHandler } from './put-resource-policy.handler';
import { PutSecretValueHandler } from './put-secret-value.handler'; import { PutSecretValueHandler } from './put-secret-value.handler';
import { Secret } from './secret.entity';
import { SecretService } from './secret.service'; import { SecretService } from './secret.service';
import { SecretsManagerHandlers } from './secrets-manager.constants'; import { SecretsManagerHandlers } from './secrets-manager.constants';
@ -53,7 +53,7 @@ const actions = [
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Secret]), PrismaModule,
AwsSharedEntitiesModule, AwsSharedEntitiesModule,
], ],
providers: [ providers: [

View File

@ -1,4 +1,4 @@
import { SnsTopic, SnsTopicSubscription } from "@prisma/client"; import { Secret, SnsTopic, SnsTopicSubscription } from "@prisma/client";
export class ArnUtil { export class ArnUtil {
@ -9,4 +9,13 @@ export class ArnUtil {
static fromTopicSub(topicSub: SnsTopicSubscription): string { static fromTopicSub(topicSub: SnsTopicSubscription): string {
return `${topicSub.topicArn}:${topicSub.id}`; 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;
}
} }