diff --git a/packages/api/src/router/deployment-version-checks/approvals.ts b/packages/api/src/router/deployment-version-checks/approvals.ts index 60650345a..4cbb29e29 100644 --- a/packages/api/src/router/deployment-version-checks/approvals.ts +++ b/packages/api/src/router/deployment-version-checks/approvals.ts @@ -84,7 +84,7 @@ export const approvalRouter = createTRPCRouter({ const { deploymentVersionId, environmentId, status, reason } = input; const record = await ctx.db - .insert(SCHEMA.policyRuleAnyApprovalRecord) + .insert(SCHEMA.deploymentVersionApprovalRecord) .values({ deploymentVersionId, userId: ctx.session.user.id, diff --git a/packages/db/src/schema/rbac.ts b/packages/db/src/schema/rbac.ts index c0cbe383a..e4a134222 100644 --- a/packages/db/src/schema/rbac.ts +++ b/packages/db/src/schema/rbac.ts @@ -31,6 +31,10 @@ export const rolePermission = pgTable( export const entityType = pgEnum("entity_type", ["user", "team"]); export const entityTypeSchema = z.enum(entityType.enumValues); export type EntityType = z.infer; +export enum EntityTypeEnum { + User = "user", + Team = "team", +} export const scopeType = pgEnum("scope_type", [ "deploymentVersion", diff --git a/packages/db/src/schema/rules/approval-any.ts b/packages/db/src/schema/rules/approval-any.ts index 2e914c9c0..6e728f9d6 100644 --- a/packages/db/src/schema/rules/approval-any.ts +++ b/packages/db/src/schema/rules/approval-any.ts @@ -1,12 +1,8 @@ import type { InferSelectModel } from "drizzle-orm"; -import { integer, pgTable, uniqueIndex } from "drizzle-orm/pg-core"; +import { integer, pgTable } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; -import { - baseApprovalRecordFields, - baseApprovalRecordValidationFields, -} from "./approval-base.js"; import { basePolicyRuleFields, basePolicyRuleValidationFields, @@ -22,18 +18,6 @@ export const policyRuleAnyApproval = pgTable("policy_rule_any_approval", { .default(1), }); -// Approval records specific to any approval rules -export const policyRuleAnyApprovalRecord = pgTable( - "policy_rule_any_approval_record", - baseApprovalRecordFields, - (t) => ({ - uniqueRuleIdUserId: uniqueIndex("unique_rule_id_user_id").on( - t.deploymentVersionId, - t.userId, - ), - }), -); - // Validation schemas export const policyRuleAnyApprovalInsertSchema = createInsertSchema( policyRuleAnyApproval, @@ -43,23 +27,12 @@ export const policyRuleAnyApprovalInsertSchema = createInsertSchema( }, ).omit({ id: true, createdAt: true }); -export const policyRuleAnyApprovalRecordInsertSchema = createInsertSchema( - policyRuleAnyApprovalRecord, - baseApprovalRecordValidationFields, -).omit({ id: true, createdAt: true, updatedAt: true }); - // Export create schemas export const createPolicyRuleAnyApproval = policyRuleAnyApprovalInsertSchema; export type CreatePolicyRuleAnyApproval = z.infer< typeof createPolicyRuleAnyApproval >; -export const createPolicyRuleAnyApprovalRecord = - policyRuleAnyApprovalRecordInsertSchema; -export type CreatePolicyRuleAnyApprovalRecord = z.infer< - typeof createPolicyRuleAnyApprovalRecord ->; - // Export update schemas export const updatePolicyRuleAnyApproval = policyRuleAnyApprovalInsertSchema.partial(); @@ -67,16 +40,7 @@ export type UpdatePolicyRuleAnyApproval = z.infer< typeof updatePolicyRuleAnyApproval >; -export const updatePolicyRuleAnyApprovalRecord = - policyRuleAnyApprovalRecordInsertSchema.partial(); -export type UpdatePolicyRuleAnyApprovalRecord = z.infer< - typeof updatePolicyRuleAnyApprovalRecord ->; - // Export model types export type PolicyRuleAnyApproval = InferSelectModel< typeof policyRuleAnyApproval >; -export type PolicyRuleAnyApprovalRecord = InferSelectModel< - typeof policyRuleAnyApprovalRecord ->; diff --git a/packages/db/src/schema/rules/approval-base.ts b/packages/db/src/schema/rules/approval-base.ts deleted file mode 100644 index 149ec1367..000000000 --- a/packages/db/src/schema/rules/approval-base.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { sql } from "drizzle-orm"; -import { pgEnum, text, timestamp, uuid } from "drizzle-orm/pg-core"; -import { z } from "zod"; - -import { user } from "../auth.js"; - -// Approval status enum -export const approvalStatus = pgEnum("approval_status", [ - "approved", - "rejected", -]); - -// Base approval record fields that all record types share -export const baseApprovalRecordFields = { - id: uuid("id").primaryKey().defaultRandom(), - - // Link to the deployment version being approved - deploymentVersionId: uuid("deployment_version_id").notNull(), - - // User who performed the approval/rejection action - userId: uuid("user_id") - .references(() => user.id) - .notNull(), - - // Status of this approval - status: approvalStatus("status").notNull(), - - // Timestamp of when the approval was performed - approvedAt: timestamp("approved_at", { withTimezone: true }).default( - sql`NULL`, - ), - - // Reason provided by approver - reason: text("reason"), - - createdAt: timestamp("created_at", { withTimezone: true }) - .notNull() - .defaultNow(), - - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date(), - ), -}; - -export enum ApprovalStatus { - Approved = "approved", - Rejected = "rejected", -} - -// Base validation fields for approval records -export const baseApprovalRecordValidationFields = { - deploymentVersionId: z.string().uuid(), - userId: z.string().uuid(), - status: z.enum(approvalStatus.enumValues), - reason: z.string().optional(), -}; diff --git a/packages/db/src/schema/rules/approval-record.ts b/packages/db/src/schema/rules/approval-record.ts new file mode 100644 index 000000000..1233c547e --- /dev/null +++ b/packages/db/src/schema/rules/approval-record.ts @@ -0,0 +1,69 @@ +import { sql } from "drizzle-orm"; +import { + pgEnum, + pgTable, + text, + timestamp, + uniqueIndex, + uuid, +} from "drizzle-orm/pg-core"; +import { z } from "zod"; + +import { user } from "../auth.js"; + +export const approvalStatus = pgEnum("approval_status", [ + "approved", + "rejected", +]); + +export const deploymentVersionApprovalRecord = pgTable( + "deployment_version_approval_record", + { + id: uuid("id").primaryKey().defaultRandom(), + + // Link to the deployment version being approved + deploymentVersionId: uuid("deployment_version_id").notNull(), + + // User who performed the approval/rejection action + userId: uuid("user_id") + .references(() => user.id) + .notNull(), + + // Status of this approval + status: approvalStatus("status").notNull(), + + // Timestamp of when the approval was performed + approvedAt: timestamp("approved_at", { withTimezone: true }).default( + sql`NULL`, + ), + + // Reason provided by approver + reason: text("reason"), + + createdAt: timestamp("created_at", { withTimezone: true }) + .notNull() + .defaultNow(), + + updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( + () => new Date(), + ), + }, + (t) => ({ + uniqueDeploymentVersionIdUserId: uniqueIndex( + "unique_deployment_version_id_user_id", + ).on(t.deploymentVersionId, t.userId), + }), +); + +// Base validation fields for approval records +export const baseApprovalRecordValidationFields = { + deploymentVersionId: z.string().uuid(), + userId: z.string().uuid(), + status: z.enum(approvalStatus.enumValues), + reason: z.string().optional(), +}; + +export enum ApprovalStatus { + Approved = "approved", + Rejected = "rejected", +} diff --git a/packages/db/src/schema/rules/approval-role.ts b/packages/db/src/schema/rules/approval-role.ts index d3c4a7e68..3619ef6af 100644 --- a/packages/db/src/schema/rules/approval-role.ts +++ b/packages/db/src/schema/rules/approval-role.ts @@ -4,10 +4,6 @@ import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; import { role } from "../rbac.js"; -import { - baseApprovalRecordFields, - baseApprovalRecordValidationFields, -} from "./approval-base.js"; import { basePolicyRuleFields, basePolicyRuleValidationFields, @@ -28,19 +24,6 @@ export const policyRuleRoleApproval = pgTable("policy_rule_role_approval", { .default(1), }); -// Approval records specific to role approval rules -export const policyRuleRoleApprovalRecord = pgTable( - "policy_rule_role_approval_record", - { - ...baseApprovalRecordFields, - - // Link to the role approval rule - ruleId: uuid("rule_id") - .notNull() - .references(() => policyRuleRoleApproval.id, { onDelete: "cascade" }), - }, -); - // Validation schemas export const policyRuleRoleApprovalInsertSchema = createInsertSchema( policyRuleRoleApproval, @@ -51,26 +34,12 @@ export const policyRuleRoleApprovalInsertSchema = createInsertSchema( }, ).omit({ id: true, createdAt: true }); -export const policyRuleRoleApprovalRecordInsertSchema = createInsertSchema( - policyRuleRoleApprovalRecord, - { - ...baseApprovalRecordValidationFields, - ruleId: z.string().uuid(), - }, -).omit({ id: true, createdAt: true, updatedAt: true }); - // Export create schemas export const createPolicyRuleRoleApproval = policyRuleRoleApprovalInsertSchema; export type CreatePolicyRuleRoleApproval = z.infer< typeof createPolicyRuleRoleApproval >; -export const createPolicyRuleRoleApprovalRecord = - policyRuleRoleApprovalRecordInsertSchema; -export type CreatePolicyRuleRoleApprovalRecord = z.infer< - typeof createPolicyRuleRoleApprovalRecord ->; - // Export update schemas export const updatePolicyRuleRoleApproval = policyRuleRoleApprovalInsertSchema.partial(); @@ -78,16 +47,7 @@ export type UpdatePolicyRuleRoleApproval = z.infer< typeof updatePolicyRuleRoleApproval >; -export const updatePolicyRuleRoleApprovalRecord = - policyRuleRoleApprovalRecordInsertSchema.partial(); -export type UpdatePolicyRuleRoleApprovalRecord = z.infer< - typeof updatePolicyRuleRoleApprovalRecord ->; - // Export model types export type PolicyRuleRoleApproval = InferSelectModel< typeof policyRuleRoleApproval >; -export type PolicyRuleRoleApprovalRecord = InferSelectModel< - typeof policyRuleRoleApprovalRecord ->; diff --git a/packages/db/src/schema/rules/approval-user.ts b/packages/db/src/schema/rules/approval-user.ts index 0036b27d0..2a1e2609d 100644 --- a/packages/db/src/schema/rules/approval-user.ts +++ b/packages/db/src/schema/rules/approval-user.ts @@ -4,10 +4,6 @@ import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; import { user } from "../auth.js"; -import { - baseApprovalRecordFields, - baseApprovalRecordValidationFields, -} from "./approval-base.js"; import { basePolicyRuleFields, basePolicyRuleValidationFields, @@ -23,19 +19,6 @@ export const policyRuleUserApproval = pgTable("policy_rule_user_approval", { .references(() => user.id), }); -// Approval records specific to user approval rules -export const policyRuleUserApprovalRecord = pgTable( - "policy_rule_user_approval_record", - { - ...baseApprovalRecordFields, - - // Link to the user approval rule - ruleId: uuid("rule_id") - .notNull() - .references(() => policyRuleUserApproval.id, { onDelete: "cascade" }), - }, -); - // Validation schemas export const policyRuleUserApprovalInsertSchema = createInsertSchema( policyRuleUserApproval, @@ -45,26 +28,12 @@ export const policyRuleUserApprovalInsertSchema = createInsertSchema( }, ).omit({ id: true, createdAt: true }); -export const policyRuleUserApprovalRecordInsertSchema = createInsertSchema( - policyRuleUserApprovalRecord, - { - ...baseApprovalRecordValidationFields, - ruleId: z.string().uuid(), - }, -).omit({ id: true, createdAt: true, updatedAt: true }); - // Export create schemas export const createPolicyRuleUserApproval = policyRuleUserApprovalInsertSchema; export type CreatePolicyRuleUserApproval = z.infer< typeof createPolicyRuleUserApproval >; -export const createPolicyRuleUserApprovalRecord = - policyRuleUserApprovalRecordInsertSchema; -export type CreatePolicyRuleUserApprovalRecord = z.infer< - typeof createPolicyRuleUserApprovalRecord ->; - // Export update schemas export const updatePolicyRuleUserApproval = policyRuleUserApprovalInsertSchema.partial(); @@ -72,16 +41,7 @@ export type UpdatePolicyRuleUserApproval = z.infer< typeof updatePolicyRuleUserApproval >; -export const updatePolicyRuleUserApprovalRecord = - policyRuleUserApprovalRecordInsertSchema.partial(); -export type UpdatePolicyRuleUserApprovalRecord = z.infer< - typeof updatePolicyRuleUserApprovalRecord ->; - // Export model types export type PolicyRuleUserApproval = InferSelectModel< typeof policyRuleUserApproval >; -export type PolicyRuleUserApprovalRecord = InferSelectModel< - typeof policyRuleUserApprovalRecord ->; diff --git a/packages/db/src/schema/rules/index.ts b/packages/db/src/schema/rules/index.ts index 04efaff96..c6ce8b7e7 100644 --- a/packages/db/src/schema/rules/index.ts +++ b/packages/db/src/schema/rules/index.ts @@ -2,10 +2,10 @@ export * from "./base.js"; export * from "./deny-window.js"; -export * from "./approval-base.js"; export * from "./approval-user.js"; export * from "./approval-team.js"; export * from "./approval-role.js"; export * from "./approval-any.js"; +export * from "./approval-record.js"; export * from "./rule-relations.js"; export * from "./deployment-selector.js"; diff --git a/packages/db/src/schema/rules/rule-relations.ts b/packages/db/src/schema/rules/rule-relations.ts index 3ae6af698..17f30345a 100644 --- a/packages/db/src/schema/rules/rule-relations.ts +++ b/packages/db/src/schema/rules/rule-relations.ts @@ -3,26 +3,16 @@ import { relations } from "drizzle-orm"; import { user } from "../auth.js"; import { deploymentVersion } from "../deployment-version.js"; import { policy } from "../policy.js"; -import { - policyRuleAnyApproval, - policyRuleAnyApprovalRecord, -} from "./approval-any.js"; -import { - policyRuleRoleApproval, - policyRuleRoleApprovalRecord, -} from "./approval-role.js"; -import { - policyRuleUserApproval, - policyRuleUserApprovalRecord, -} from "./approval-user.js"; +import { policyRuleAnyApproval } from "./approval-any.js"; +import { deploymentVersionApprovalRecord } from "./approval-record.js"; +import { policyRuleRoleApproval } from "./approval-role.js"; +import { policyRuleUserApproval } from "./approval-user.js"; import { policyRuleDenyWindow } from "./deny-window.js"; import { policyRuleDeploymentVersionSelector } from "./deployment-selector.js"; // User relations to approval records export const userApprovalRelations = relations(user, ({ many }) => ({ - userApprovalRecords: many(policyRuleUserApprovalRecord), - roleApprovalRecords: many(policyRuleRoleApprovalRecord), - anyApprovalRecords: many(policyRuleAnyApprovalRecord), + approvalRecords: many(deploymentVersionApprovalRecord), })); // Approval user relations @@ -36,24 +26,6 @@ export const policyRuleUserApprovalRelations = relations( }), ); -export const policyRuleUserApprovalRecordRelations = relations( - policyRuleUserApprovalRecord, - ({ one }) => ({ - user: one(user, { - fields: [policyRuleUserApprovalRecord.userId], - references: [user.id], - }), - deploymentVersion: one(deploymentVersion, { - fields: [policyRuleUserApprovalRecord.deploymentVersionId], - references: [deploymentVersion.id], - }), - rule: one(policyRuleUserApproval, { - fields: [policyRuleUserApprovalRecord.ruleId], - references: [policyRuleUserApproval.id], - }), - }), -); - // Approval role relations export const policyRuleRoleApprovalRelations = relations( policyRuleRoleApproval, @@ -65,24 +37,6 @@ export const policyRuleRoleApprovalRelations = relations( }), ); -export const policyRuleRoleApprovalRecordRelations = relations( - policyRuleRoleApprovalRecord, - ({ one }) => ({ - user: one(user, { - fields: [policyRuleRoleApprovalRecord.userId], - references: [user.id], - }), - deploymentVersion: one(deploymentVersion, { - fields: [policyRuleRoleApprovalRecord.deploymentVersionId], - references: [deploymentVersion.id], - }), - rule: one(policyRuleRoleApproval, { - fields: [policyRuleRoleApprovalRecord.ruleId], - references: [policyRuleRoleApproval.id], - }), - }), -); - // Approval any relations export const policyRuleAnyApprovalRelations = relations( policyRuleAnyApproval, @@ -94,20 +48,6 @@ export const policyRuleAnyApprovalRelations = relations( }), ); -export const policyRuleAnyApprovalRecordRelations = relations( - policyRuleAnyApprovalRecord, - ({ one }) => ({ - user: one(user, { - fields: [policyRuleAnyApprovalRecord.userId], - references: [user.id], - }), - deploymentVersion: one(deploymentVersion, { - fields: [policyRuleAnyApprovalRecord.deploymentVersionId], - references: [deploymentVersion.id], - }), - }), -); - export const policyRuleDenyWindowRelations = relations( policyRuleDenyWindow, ({ one }) => ({ @@ -127,3 +67,17 @@ export const policyDeploymentVersionSelectorRelations = relations( }), }), ); + +export const deploymentVersionApprovalRecordRelations = relations( + deploymentVersionApprovalRecord, + ({ one }) => ({ + deploymentVersion: one(deploymentVersion, { + fields: [deploymentVersionApprovalRecord.deploymentVersionId], + references: [deploymentVersion.id], + }), + user: one(user, { + fields: [deploymentVersionApprovalRecord.userId], + references: [user.id], + }), + }), +); diff --git a/packages/rule-engine/src/manager/version-manager-rules.ts b/packages/rule-engine/src/manager/version-manager-rules.ts index 3d722e37d..c13d0d995 100644 --- a/packages/rule-engine/src/manager/version-manager-rules.ts +++ b/packages/rule-engine/src/manager/version-manager-rules.ts @@ -3,8 +3,8 @@ import type { Version } from "./version-rule-engine"; import { DeploymentDenyRule } from "../rules/deployment-deny-rule.js"; import { getAnyApprovalRecords, - getRoleApprovalRecords, - getUserApprovalRecords, + getRoleApprovalRecordsFunc, + getUserApprovalRecordsFunc, VersionApprovalRule, } from "../rules/version-approval-rule.js"; @@ -37,10 +37,10 @@ const versionRoleApprovalRule = ( ) => { if (approvalRules == null) return []; return approvalRules.map( - (approval) => + ({ roleId, requiredApprovalsCount }) => new VersionApprovalRule({ - minApprovals: approval.requiredApprovalsCount, - getApprovalRecords: getRoleApprovalRecords, + minApprovals: requiredApprovalsCount, + getApprovalRecords: getRoleApprovalRecordsFunc(roleId), }), ); }; @@ -50,10 +50,10 @@ const versionUserApprovalRule = ( ) => { if (approvalRules == null) return []; return approvalRules.map( - () => + ({ userId }) => new VersionApprovalRule({ minApprovals: 1, - getApprovalRecords: getUserApprovalRecords, + getApprovalRecords: getUserApprovalRecordsFunc(userId), }), ); }; diff --git a/packages/rule-engine/src/rules/version-approval-rule.ts b/packages/rule-engine/src/rules/version-approval-rule.ts index 83c4ccca4..f9ba2ab69 100644 --- a/packages/rule-engine/src/rules/version-approval-rule.ts +++ b/packages/rule-engine/src/rules/version-approval-rule.ts @@ -1,6 +1,6 @@ import _ from "lodash"; -import { inArray } from "@ctrlplane/db"; +import { and, eq, inArray } from "@ctrlplane/db"; import { db } from "@ctrlplane/db/client"; import * as schema from "@ctrlplane/db/schema"; @@ -12,7 +12,7 @@ import type { } from "../types.js"; type Record = { - versionId: string; + deploymentVersionId: string; status: "approved" | "rejected"; userId: string; reason: string | null; @@ -20,7 +20,7 @@ type Record = { export type GetApprovalRecordsFunc = ( context: RuleEngineContext, - versionIds: string[], + deploymentVersionIds: string[], ) => Promise; type VersionApprovalRuleOptions = { @@ -39,17 +39,19 @@ export class VersionApprovalRule implements FilterRule { candidates: Version[], ): Promise> { const rejectionReasons = new Map(); - const versionIds = _(candidates) + const deploymentVersionIds = _(candidates) .map((r) => r.id) .uniq() .value(); const approvalRecords = await this.options.getApprovalRecords( context, - versionIds, + deploymentVersionIds, ); const allowedCandidates = candidates.filter((release) => { - const records = approvalRecords.filter((r) => r.versionId === release.id); + const records = approvalRecords.filter( + (r) => r.deploymentVersionId === release.id, + ); const approvals = records.filter((r) => r.status === "approved"); const rejections = records.filter((r) => r.status === "rejected"); @@ -71,48 +73,53 @@ export class VersionApprovalRule implements FilterRule { export const getAnyApprovalRecords: GetApprovalRecordsFunc = async ( _: RuleEngineContext, - versionIds: string[], -) => { - const records = await db.query.policyRuleAnyApprovalRecord.findMany({ + deploymentVersionIds: string[], +) => + db.query.deploymentVersionApprovalRecord.findMany({ where: inArray( - schema.policyRuleAnyApprovalRecord.deploymentVersionId, - versionIds, + schema.deploymentVersionApprovalRecord.deploymentVersionId, + deploymentVersionIds, ), }); - return records.map((record) => ({ - ...record, - versionId: record.deploymentVersionId, - })); -}; - -export const getRoleApprovalRecords: GetApprovalRecordsFunc = async ( - _: RuleEngineContext, - versionIds: string[], -) => { - const records = await db.query.policyRuleRoleApprovalRecord.findMany({ - where: inArray( - schema.policyRuleRoleApprovalRecord.deploymentVersionId, - versionIds, - ), - }); - return records.map((record) => ({ - ...record, - versionId: record.deploymentVersionId, - })); -}; -export const getUserApprovalRecords: GetApprovalRecordsFunc = async ( - _: RuleEngineContext, - versionIds: string[], -) => { - const records = await db.query.policyRuleUserApprovalRecord.findMany({ - where: inArray( - schema.policyRuleUserApprovalRecord.deploymentVersionId, - versionIds, - ), - }); - return records.map((record) => ({ - ...record, - versionId: record.deploymentVersionId, - })); -}; +export const getRoleApprovalRecordsFunc = + (roleId: string): GetApprovalRecordsFunc => + async (_, deploymentVersionIds) => { + const recordResults = await db + .select() + .from(schema.deploymentVersionApprovalRecord) + .innerJoin( + schema.entityRole, + eq( + schema.entityRole.entityId, + schema.deploymentVersionApprovalRecord.userId, + ), + ) + .where( + and( + inArray( + schema.deploymentVersionApprovalRecord.deploymentVersionId, + deploymentVersionIds, + ), + eq(schema.entityRole.entityType, schema.EntityTypeEnum.User), + eq(schema.entityRole.roleId, roleId), + ), + ); + + return recordResults.map( + (record) => record.deployment_version_approval_record, + ); + }; + +export const getUserApprovalRecordsFunc = + (userId: string): GetApprovalRecordsFunc => + async (_, deploymentVersionIds) => + db.query.deploymentVersionApprovalRecord.findMany({ + where: and( + inArray( + schema.deploymentVersionApprovalRecord.deploymentVersionId, + deploymentVersionIds, + ), + eq(schema.deploymentVersionApprovalRecord.userId, userId), + ), + });