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?
}
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

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 { 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 = {
@ -34,7 +37,7 @@ export class CreateSecretHandler extends AbstractActionHandler<QueryParams> {
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<QueryParams> {
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 * 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 = {
@ -15,6 +17,7 @@ export class DeleteSecretHandler extends AbstractActionHandler {
constructor(
private readonly secretService: SecretService,
private readonly prismaService: PrismaService,
) {
super();
}
@ -28,7 +31,7 @@ export class DeleteSecretHandler 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,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,
}

View File

@ -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;
@ -15,8 +15,7 @@ type QueryParams = {
export class DescribeSecretHandler extends AbstractActionHandler {
constructor(
@InjectRepository(Secret)
private readonly secretRepo: Repository<Secret>,
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,

View File

@ -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<Secret>,
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,
}

View File

@ -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,

View File

@ -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<Secret>,
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,
}
}

View File

@ -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<QueryParams> {
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<QueryParams> {
}
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: [] }
}
}

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 { 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<Secret>,
private readonly prismaService: PrismaService,
) {}
async findLatestByNameAndRegion(name: string, region: string): Promise<Secret> {
return await this.secretRepo.findOne({ where: { name, region }, order: { createdAt: 'DESC' } });
async findLatestByNameAndRegion(name: string, region: string): Promise<Secret | null> {
return await this.prismaService.secret.findFirst({ where: { name, region }, orderBy: { createdAt: 'desc' } });
}
async findByNameAndVersion(name: string, versionId: string): Promise<Secret> {
// 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<Secret | null> {
return await this.prismaService.secret.findFirst({ where: { name, versionId } });
}
async create(dto: CreateSecretDto): Promise<Secret> {
return await this.secretRepo.create({
...dto,
versionId: dto.versionId ?? uuid.v4(),
}).save();
async create(data: Prisma.SecretCreateInput): Promise<Secret> {
return await this.prismaService.secret.create({
data: {
...data,
versionId: data.versionId ?? randomUUID(),
}
});
}
}

View File

@ -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: [

View File

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