mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 08:27:36 +00:00
Merge pull request #2061 from akhilmhdh/feat/secret-approval-grouo
Secret approval with groups
This commit is contained in:
@@ -0,0 +1,188 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
// migrate secret approval policy approvers to user id
|
||||||
|
const hasApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||||
|
const hasApproverId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverId");
|
||||||
|
if (!hasApproverUserId) {
|
||||||
|
// add the new fields
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
|
||||||
|
// if (hasApproverId) tb.setNullable("approverId");
|
||||||
|
tb.uuid("approverUserId");
|
||||||
|
tb.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
|
||||||
|
// convert project membership id => user id
|
||||||
|
await knex(TableName.SecretApprovalPolicyApprover).update({
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
approverUserId: knex(TableName.ProjectMembership)
|
||||||
|
.select("userId")
|
||||||
|
.where("id", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverId`]))
|
||||||
|
});
|
||||||
|
// drop the old field
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
|
||||||
|
if (hasApproverId) tb.dropColumn("approverId");
|
||||||
|
tb.uuid("approverUserId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate secret approval request committer and statusChangeBy to user id
|
||||||
|
const hasSecretApprovalRequestTable = await knex.schema.hasTable(TableName.SecretApprovalRequest);
|
||||||
|
const hasCommitterUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId");
|
||||||
|
const hasCommitterId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerId");
|
||||||
|
const hasStatusChangeBy = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeBy");
|
||||||
|
const hasStatusChangedByUserId = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalRequest,
|
||||||
|
"statusChangedByUserId"
|
||||||
|
);
|
||||||
|
if (hasSecretApprovalRequestTable) {
|
||||||
|
// new fields
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
|
||||||
|
// if (hasCommitterId) tb.setNullable("committerId");
|
||||||
|
if (!hasCommitterUserId) {
|
||||||
|
tb.uuid("committerUserId");
|
||||||
|
tb.foreign("committerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
|
}
|
||||||
|
if (!hasStatusChangedByUserId) {
|
||||||
|
tb.uuid("statusChangedByUserId");
|
||||||
|
tb.foreign("statusChangedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// copy the assigned project membership => user id to new fields
|
||||||
|
await knex(TableName.SecretApprovalRequest).update({
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
committerUserId: knex(TableName.ProjectMembership)
|
||||||
|
.select("userId")
|
||||||
|
.where("id", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerId`])),
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
statusChangedByUserId: knex(TableName.ProjectMembership)
|
||||||
|
.select("userId")
|
||||||
|
.where("id", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangeBy`]))
|
||||||
|
});
|
||||||
|
// drop old fields
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
|
||||||
|
if (hasStatusChangeBy) tb.dropColumn("statusChangeBy");
|
||||||
|
if (hasCommitterId) tb.dropColumn("committerId");
|
||||||
|
tb.uuid("committerUserId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate secret approval request reviewer to user id
|
||||||
|
const hasMemberId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "member");
|
||||||
|
const hasReviewerUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "reviewerUserId");
|
||||||
|
if (!hasReviewerUserId) {
|
||||||
|
// new fields
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
|
||||||
|
// if (hasMemberId) tb.setNullable("member");
|
||||||
|
tb.uuid("reviewerUserId");
|
||||||
|
tb.foreign("reviewerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
|
||||||
|
});
|
||||||
|
// copy project membership => user id to new fields
|
||||||
|
await knex(TableName.SecretApprovalRequestReviewer).update({
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
reviewerUserId: knex(TableName.ProjectMembership)
|
||||||
|
.select("userId")
|
||||||
|
.where("id", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.member`]))
|
||||||
|
});
|
||||||
|
// drop table
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
|
||||||
|
if (hasMemberId) tb.dropColumn("member");
|
||||||
|
tb.uuid("reviewerUserId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasApproverUserId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverUserId");
|
||||||
|
const hasApproverId = await knex.schema.hasColumn(TableName.SecretApprovalPolicyApprover, "approverId");
|
||||||
|
if (hasApproverUserId) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
|
||||||
|
if (!hasApproverId) {
|
||||||
|
tb.uuid("approverId");
|
||||||
|
tb.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasApproverId) {
|
||||||
|
await knex(TableName.SecretApprovalPolicyApprover).update({
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
approverId: knex(TableName.ProjectMembership)
|
||||||
|
.select("id")
|
||||||
|
.where("userId", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverUserId`]))
|
||||||
|
});
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
|
||||||
|
tb.dropColumn("approverUserId");
|
||||||
|
tb.uuid("approverId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSecretApprovalRequestTable = await knex.schema.hasTable(TableName.SecretApprovalRequest);
|
||||||
|
const hasCommitterUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerUserId");
|
||||||
|
const hasCommitterId = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "committerId");
|
||||||
|
const hasStatusChangeBy = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangeBy");
|
||||||
|
const hasStatusChangedByUser = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "statusChangedByUserId");
|
||||||
|
if (hasSecretApprovalRequestTable) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
|
||||||
|
// if (hasCommitterId) tb.uuid("committerId").notNullable().alter();
|
||||||
|
if (!hasCommitterId) {
|
||||||
|
tb.uuid("committerId");
|
||||||
|
tb.foreign("committerId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||||
|
}
|
||||||
|
if (!hasStatusChangeBy) {
|
||||||
|
tb.uuid("statusChangeBy");
|
||||||
|
tb.foreign("statusChangeBy").references("id").inTable(TableName.ProjectMembership).onDelete("SET NULL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex(TableName.SecretApprovalRequest).update({
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
committerId: knex(TableName.ProjectMembership)
|
||||||
|
.select("id")
|
||||||
|
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])),
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
statusChangeBy: knex(TableName.ProjectMembership)
|
||||||
|
.select("id")
|
||||||
|
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangedByUserId`]))
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
|
||||||
|
if (hasCommitterUserId) tb.dropColumn("committerUserId");
|
||||||
|
if (hasStatusChangedByUser) tb.dropColumn("statusChangedByUserId");
|
||||||
|
if (hasCommitterId) tb.uuid("committerId").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMemberId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "member");
|
||||||
|
const hasReviewerUserId = await knex.schema.hasColumn(TableName.SecretApprovalRequestReviewer, "reviewerUserId");
|
||||||
|
if (hasReviewerUserId) {
|
||||||
|
if (!hasMemberId) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
|
||||||
|
// if (hasMemberId) tb.uuid("member").notNullable().alter();
|
||||||
|
tb.uuid("member");
|
||||||
|
tb.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await knex(TableName.SecretApprovalRequestReviewer).update({
|
||||||
|
// eslint-disable-next-line
|
||||||
|
// @ts-ignore because generate schema happens after this
|
||||||
|
member: knex(TableName.ProjectMembership)
|
||||||
|
.select("id")
|
||||||
|
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`]))
|
||||||
|
});
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
|
||||||
|
tb.uuid("member").notNullable().alter();
|
||||||
|
tb.dropColumn("reviewerUserId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,10 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const SecretApprovalPoliciesApproversSchema = z.object({
|
export const SecretApprovalPoliciesApproversSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
approverId: z.string().uuid(),
|
|
||||||
policyId: z.string().uuid(),
|
policyId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
approverUserId: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
export type TSecretApprovalPoliciesApprovers = z.infer<typeof SecretApprovalPoliciesApproversSchema>;
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
|
|
||||||
export const SecretApprovalRequestsReviewersSchema = z.object({
|
export const SecretApprovalRequestsReviewersSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
member: z.string().uuid(),
|
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
requestId: z.string().uuid(),
|
requestId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
reviewerUserId: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
export type TSecretApprovalRequestsReviewers = z.infer<typeof SecretApprovalRequestsReviewersSchema>;
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ export const SecretApprovalRequestsSchema = z.object({
|
|||||||
conflicts: z.unknown().nullable().optional(),
|
conflicts: z.unknown().nullable().optional(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
statusChangeBy: z.string().uuid().nullable().optional(),
|
|
||||||
committerId: z.string().uuid(),
|
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
isReplicated: z.boolean().nullable().optional()
|
isReplicated: z.boolean().nullable().optional(),
|
||||||
|
committerUserId: z.string().uuid(),
|
||||||
|
statusChangedByUserId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
|
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||||
approvers: z.string().array().min(1),
|
approverUserIds: z.string().array().min(1),
|
||||||
approvals: z.number().min(1).default(1)
|
approvals: z.number().min(1).default(1)
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||||
path: ["approvals"],
|
path: ["approvals"],
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
}),
|
}),
|
||||||
@@ -66,7 +66,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
body: z
|
body: z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
approvers: z.string().array().min(1),
|
approverUserIds: z.string().array().min(1),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
secretPath: z
|
secretPath: z
|
||||||
.string()
|
.string()
|
||||||
@@ -74,7 +74,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.nullable()
|
.nullable()
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||||
path: ["approvals"],
|
path: ["approvals"],
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
}),
|
}),
|
||||||
@@ -139,7 +139,15 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array()
|
approvals: sapPubSchema
|
||||||
|
.extend({
|
||||||
|
userApprovers: z
|
||||||
|
.object({
|
||||||
|
userId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -170,7 +178,11 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
policy: sapPubSchema.merge(z.object({ approvers: z.string().array() })).optional()
|
policy: sapPubSchema
|
||||||
|
.extend({
|
||||||
|
userApprovers: z.object({ userId: z.string() }).array()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
SecretApprovalRequestsSecretsSchema,
|
SecretApprovalRequestsSecretsSchema,
|
||||||
SecretsSchema,
|
SecretsSchema,
|
||||||
SecretTagsSchema,
|
SecretTagsSchema,
|
||||||
SecretVersionsSchema
|
SecretVersionsSchema,
|
||||||
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
@@ -14,6 +15,15 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const approvalRequestUser = z.object({ userId: z.string() }).merge(
|
||||||
|
UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
username: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretApprovalRequestRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -41,9 +51,10 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
approvers: z.string().array(),
|
approvers: z.string().array(),
|
||||||
secretPath: z.string().optional().nullable()
|
secretPath: z.string().optional().nullable()
|
||||||
}),
|
}),
|
||||||
|
committerUser: approvalRequestUser,
|
||||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
reviewers: z.object({ member: z.string(), status: z.string() }).array(),
|
reviewers: z.object({ userId: z.string(), status: z.string() }).array(),
|
||||||
approvers: z.string().array()
|
approvers: z.string().array()
|
||||||
}).array()
|
}).array()
|
||||||
})
|
})
|
||||||
@@ -195,7 +206,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED,
|
type: isClosing ? EventType.SECRET_APPROVAL_CLOSED : EventType.SECRET_APPROVAL_REOPENED,
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
metadata: {
|
metadata: {
|
||||||
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangeBy as string,
|
[isClosing ? ("closedBy" as const) : ("reopenedBy" as const)]: approval.statusChangedByUserId as string,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -216,6 +227,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:id",
|
url: "/:id",
|
||||||
@@ -235,12 +247,13 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: z.string().array(),
|
approvers: approvalRequestUser.array(),
|
||||||
secretPath: z.string().optional().nullable()
|
secretPath: z.string().optional().nullable()
|
||||||
}),
|
}),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
reviewers: z.object({ member: z.string(), status: z.string() }).array(),
|
statusChangedByUser: approvalRequestUser.optional(),
|
||||||
approvers: z.string().array(),
|
committerUser: approvalRequestUser,
|
||||||
|
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
|
||||||
secretPath: z.string(),
|
secretPath: z.string(),
|
||||||
commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true })
|
commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true })
|
||||||
.merge(
|
.merge(
|
||||||
|
|||||||
@@ -1,49 +1,59 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName, TSecretApprovalPolicies } from "@app/db/schemas";
|
import { SecretApprovalPoliciesSchema, TableName, TSecretApprovalPolicies } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
|
||||||
|
|
||||||
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
|
export type TSecretApprovalPolicyDALFactory = ReturnType<typeof secretApprovalPolicyDALFactory>;
|
||||||
|
|
||||||
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
||||||
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
|
const secretApprovalPolicyOrm = ormify(db, TableName.SecretApprovalPolicy);
|
||||||
|
|
||||||
const sapFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
|
const secretApprovalPolicyFindQuery = (tx: Knex, filter: TFindFilter<TSecretApprovalPolicies>) =>
|
||||||
tx(TableName.SecretApprovalPolicy)
|
tx(TableName.SecretApprovalPolicy)
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.SecretApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
.join(
|
.leftJoin(
|
||||||
TableName.SecretApprovalPolicyApprover,
|
TableName.SecretApprovalPolicyApprover,
|
||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
.select(tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover))
|
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
|
||||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
.select(
|
||||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
tx.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
tx.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||||
|
tx.ref("projectId").withSchema(TableName.Environment)
|
||||||
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
|
.select(selectAllTableCols(TableName.SecretApprovalPolicy))
|
||||||
.orderBy("createdAt", "asc");
|
.orderBy("createdAt", "asc");
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await sapFindQuery(tx || db.replicaNode(), {
|
const doc = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), {
|
||||||
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
|
[`${TableName.SecretApprovalPolicy}.id` as "id"]: id
|
||||||
});
|
});
|
||||||
const formatedDoc = mergeOneToManyRelation(
|
const formatedDoc = sqlNestRelationships({
|
||||||
doc,
|
data: doc,
|
||||||
"id",
|
key: "id",
|
||||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
parentMapper: (data) => ({
|
||||||
...el,
|
environment: { id: data.envId, name: data.envName, slug: data.envSlug },
|
||||||
envId,
|
projectId: data.projectId,
|
||||||
environment: { id: envId, name, slug }
|
...SecretApprovalPoliciesSchema.parse(data)
|
||||||
}),
|
}),
|
||||||
({ approverId }) => approverId,
|
childrenMapper: [
|
||||||
"approvers"
|
{
|
||||||
);
|
key: "approverUserId",
|
||||||
|
label: "userApprovers" as const,
|
||||||
|
mapper: ({ approverUserId }) => ({
|
||||||
|
userId: approverUserId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
return formatedDoc?.[0];
|
return formatedDoc?.[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindById" });
|
throw new DatabaseError({ error, name: "FindById" });
|
||||||
@@ -52,18 +62,25 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
const find = async (filter: TFindFilter<TSecretApprovalPolicies & { projectId: string }>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await sapFindQuery(tx || db.replicaNode(), filter);
|
const docs = await secretApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
|
||||||
const formatedDoc = mergeOneToManyRelation(
|
const formatedDoc = sqlNestRelationships({
|
||||||
docs,
|
data: docs,
|
||||||
"id",
|
key: "id",
|
||||||
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
|
parentMapper: (data) => ({
|
||||||
...el,
|
environment: { id: data.envId, name: data.envName, slug: data.envSlug },
|
||||||
envId,
|
projectId: data.projectId,
|
||||||
environment: { id: envId, name, slug }
|
...SecretApprovalPoliciesSchema.parse(data)
|
||||||
}),
|
}),
|
||||||
({ approverId }) => approverId,
|
childrenMapper: [
|
||||||
"approvers"
|
{
|
||||||
);
|
key: "approverUserId",
|
||||||
|
label: "userApprovers" as const,
|
||||||
|
mapper: ({ approverUserId }) => ({
|
||||||
|
userId: approverUserId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
return formatedDoc;
|
return formatedDoc;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find" });
|
throw new DatabaseError({ error, name: "Find" });
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { containsGlobPatterns } from "@app/lib/picomatch";
|
import { containsGlobPatterns } from "@app/lib/picomatch";
|
||||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
|
||||||
|
|
||||||
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
|
||||||
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
|
||||||
@@ -29,7 +28,6 @@ type TSecretApprovalPolicyServiceFactoryDep = {
|
|||||||
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
secretApprovalPolicyDAL: TSecretApprovalPolicyDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
|
||||||
@@ -38,8 +36,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
secretApprovalPolicyDAL,
|
secretApprovalPolicyDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
secretApprovalPolicyApproverDAL,
|
secretApprovalPolicyApproverDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL
|
||||||
projectMembershipDAL
|
|
||||||
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
}: TSecretApprovalPolicyServiceFactoryDep) => {
|
||||||
const createSecretApprovalPolicy = async ({
|
const createSecretApprovalPolicy = async ({
|
||||||
name,
|
name,
|
||||||
@@ -48,12 +45,12 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approverUserIds,
|
||||||
projectId,
|
projectId,
|
||||||
secretPath,
|
secretPath,
|
||||||
environment
|
environment
|
||||||
}: TCreateSapDTO) => {
|
}: TCreateSapDTO) => {
|
||||||
if (approvals > approvers.length)
|
if (approvals > approverUserIds.length)
|
||||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
@@ -70,13 +67,6 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||||
|
|
||||||
const secretApprovers = await projectMembershipDAL.find({
|
|
||||||
projectId,
|
|
||||||
$in: { id: approvers }
|
|
||||||
});
|
|
||||||
if (secretApprovers.length !== approvers.length)
|
|
||||||
throw new BadRequestError({ message: "Approver not found in project" });
|
|
||||||
|
|
||||||
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
const secretApproval = await secretApprovalPolicyDAL.transaction(async (tx) => {
|
||||||
const doc = await secretApprovalPolicyDAL.create(
|
const doc = await secretApprovalPolicyDAL.create(
|
||||||
{
|
{
|
||||||
@@ -88,8 +78,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await secretApprovalPolicyApproverDAL.insertMany(
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
secretApprovers.map(({ id }) => ({
|
approverUserIds.map((approverUserId) => ({
|
||||||
approverId: id,
|
approverUserId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
@@ -100,7 +90,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateSecretApprovalPolicy = async ({
|
const updateSecretApprovalPolicy = async ({
|
||||||
approvers,
|
approverUserIds,
|
||||||
secretPath,
|
secretPath,
|
||||||
name,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -132,22 +122,11 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
if (approvers) {
|
if (approverUserIds) {
|
||||||
const secretApprovers = await projectMembershipDAL.find(
|
|
||||||
{
|
|
||||||
projectId: secretApprovalPolicy.projectId,
|
|
||||||
$in: { id: approvers }
|
|
||||||
},
|
|
||||||
{ tx }
|
|
||||||
);
|
|
||||||
if (secretApprovers.length !== approvers.length)
|
|
||||||
throw new BadRequestError({ message: "Approver not found in project" });
|
|
||||||
if (doc.approvals > secretApprovers.length)
|
|
||||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
|
||||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||||
await secretApprovalPolicyApproverDAL.insertMany(
|
await secretApprovalPolicyApproverDAL.insertMany(
|
||||||
secretApprovers.map(({ id }) => ({
|
approverUserIds.map((approverUserId) => ({
|
||||||
approverId: id,
|
approverUserId,
|
||||||
policyId: doc.id
|
policyId: doc.id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export type TCreateSapDTO = {
|
|||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: string[];
|
approverUserIds: string[];
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
name: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -13,7 +13,7 @@ export type TUpdateSapDTO = {
|
|||||||
secretPolicyId: string;
|
secretPolicyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
approvers: string[];
|
approverUserIds: string[];
|
||||||
name?: string;
|
name?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
SecretApprovalRequestsSchema,
|
SecretApprovalRequestsSchema,
|
||||||
TableName,
|
TableName,
|
||||||
TSecretApprovalRequests,
|
TSecretApprovalRequests,
|
||||||
TSecretApprovalRequestsSecrets
|
TSecretApprovalRequestsSecrets,
|
||||||
|
TUsers
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships, stripUndefinedInWhere, TFindFilter } from "@app/lib/knex";
|
import { ormify, selectAllTableCols, sqlNestRelationships, stripUndefinedInWhere, TFindFilter } from "@app/lib/knex";
|
||||||
@@ -16,7 +17,7 @@ export type TSecretApprovalRequestDALFactory = ReturnType<typeof secretApprovalR
|
|||||||
|
|
||||||
type TFindQueryFilter = {
|
type TFindQueryFilter = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
membershipId: string;
|
userId: string;
|
||||||
status?: RequestState;
|
status?: RequestState;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
committer?: string;
|
committer?: string;
|
||||||
@@ -37,27 +38,63 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalRequest}.policyId`,
|
`${TableName.SecretApprovalRequest}.policyId`,
|
||||||
`${TableName.SecretApprovalPolicy}.id`
|
`${TableName.SecretApprovalPolicy}.id`
|
||||||
)
|
)
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("statusChangedByUser"),
|
||||||
|
`${TableName.SecretApprovalRequest}.statusChangedByUserId`,
|
||||||
|
`statusChangedByUser.id`
|
||||||
|
)
|
||||||
|
.join<TUsers>(
|
||||||
|
db(TableName.Users).as("committerUser"),
|
||||||
|
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||||
|
`committerUser.id`
|
||||||
|
)
|
||||||
.join(
|
.join(
|
||||||
TableName.SecretApprovalPolicyApprover,
|
TableName.SecretApprovalPolicyApprover,
|
||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.join<TUsers>(
|
||||||
|
db(TableName.Users).as("secretApprovalPolicyApproverUser"),
|
||||||
|
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||||
|
"secretApprovalPolicyApproverUser.id"
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretApprovalRequestReviewer,
|
TableName.SecretApprovalRequestReviewer,
|
||||||
`${TableName.SecretApprovalRequest}.id`,
|
`${TableName.SecretApprovalRequest}.id`,
|
||||||
`${TableName.SecretApprovalRequestReviewer}.requestId`
|
`${TableName.SecretApprovalRequestReviewer}.requestId`
|
||||||
)
|
)
|
||||||
|
.leftJoin<TUsers>(
|
||||||
|
db(TableName.Users).as("secretApprovalReviewerUser"),
|
||||||
|
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
||||||
|
`secretApprovalReviewerUser.id`
|
||||||
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
tx.ref("member").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
|
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||||
|
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||||
|
tx.ref("firstName").withSchema("secretApprovalPolicyApproverUser").as("approverFirstName"),
|
||||||
|
tx.ref("lastName").withSchema("secretApprovalPolicyApproverUser").as("approverLastName"),
|
||||||
|
tx.ref("email").withSchema("statusChangedByUser").as("statusChangedByUserEmail"),
|
||||||
|
tx.ref("username").withSchema("statusChangedByUser").as("statusChangedByUserUsername"),
|
||||||
|
tx.ref("firstName").withSchema("statusChangedByUser").as("statusChangedByUserFirstName"),
|
||||||
|
tx.ref("lastName").withSchema("statusChangedByUser").as("statusChangedByUserLastName"),
|
||||||
|
tx.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||||
|
tx.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||||
|
tx.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
|
tx.ref("lastName").withSchema("committerUser").as("committerUserLastName"),
|
||||||
|
tx.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||||
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
tx.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
|
tx.ref("email").withSchema("secretApprovalReviewerUser").as("reviewerEmail"),
|
||||||
|
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||||
|
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||||
|
tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"),
|
||||||
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||||
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||||
tx.ref("projectId").withSchema(TableName.Environment),
|
tx.ref("projectId").withSchema(TableName.Environment),
|
||||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
|
||||||
tx.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
@@ -71,6 +108,22 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
...SecretApprovalRequestsSchema.parse(el),
|
...SecretApprovalRequestsSchema.parse(el),
|
||||||
projectId: el.projectId,
|
projectId: el.projectId,
|
||||||
environment: el.environment,
|
environment: el.environment,
|
||||||
|
statusChangedByUser: el.statusChangedByUserId
|
||||||
|
? {
|
||||||
|
userId: el.statusChangedByUserId,
|
||||||
|
email: el.statusChangedByUserEmail,
|
||||||
|
firstName: el.statusChangedByUserFirstName,
|
||||||
|
lastName: el.statusChangedByUserLastName,
|
||||||
|
username: el.statusChangedByUserUsername
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
committerUser: {
|
||||||
|
userId: el.committerUserId,
|
||||||
|
email: el.committerUserEmail,
|
||||||
|
firstName: el.committerUserFirstName,
|
||||||
|
lastName: el.committerUserLastName,
|
||||||
|
username: el.committerUserUsername
|
||||||
|
},
|
||||||
policy: {
|
policy: {
|
||||||
id: el.policyId,
|
id: el.policyId,
|
||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
@@ -80,11 +133,34 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "reviewerMemberId",
|
key: "reviewerUserId",
|
||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
|
mapper: ({
|
||||||
|
reviewerUserId: userId,
|
||||||
|
reviewerStatus: status,
|
||||||
|
reviewerEmail: email,
|
||||||
|
reviewerLastName: lastName,
|
||||||
|
reviewerUsername: username,
|
||||||
|
reviewerFirstName: firstName
|
||||||
|
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
|
||||||
},
|
},
|
||||||
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
|
{
|
||||||
|
key: "approverUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({
|
||||||
|
approverUserId,
|
||||||
|
approverEmail: email,
|
||||||
|
approverUsername: username,
|
||||||
|
approverLastName: lastName,
|
||||||
|
approverFirstName: firstName
|
||||||
|
}) => ({
|
||||||
|
userId: approverUserId,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
username
|
||||||
|
})
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
if (!formatedDoc?.[0]) return;
|
if (!formatedDoc?.[0]) return;
|
||||||
@@ -97,7 +173,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => {
|
const findProjectRequestCount = async (projectId: string, userId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db)
|
const docs = await (tx || db)
|
||||||
.with(
|
.with(
|
||||||
@@ -114,8 +190,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
.andWhere(
|
.andWhere(
|
||||||
(bd) =>
|
(bd) =>
|
||||||
void bd
|
void bd
|
||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||||
)
|
)
|
||||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||||
@@ -142,7 +218,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const findByProjectId = async (
|
const findByProjectId = async (
|
||||||
{ status, limit = 20, offset = 0, projectId, committer, environment, membershipId }: TFindQueryFilter,
|
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@@ -161,6 +237,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.SecretApprovalPolicy}.id`,
|
`${TableName.SecretApprovalPolicy}.id`,
|
||||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||||
)
|
)
|
||||||
|
.join<TUsers>(
|
||||||
|
db(TableName.Users).as("committerUser"),
|
||||||
|
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||||
|
`committerUser.id`
|
||||||
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretApprovalRequestReviewer,
|
TableName.SecretApprovalRequestReviewer,
|
||||||
`${TableName.SecretApprovalRequest}.id`,
|
`${TableName.SecretApprovalRequest}.id`,
|
||||||
@@ -176,20 +257,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
projectId,
|
projectId,
|
||||||
[`${TableName.Environment}.slug` as "slug"]: environment,
|
[`${TableName.Environment}.slug` as "slug"]: environment,
|
||||||
[`${TableName.SecretApprovalRequest}.status`]: status,
|
[`${TableName.SecretApprovalRequest}.status`]: status,
|
||||||
committerId: committer
|
committerUserId: committer
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.andWhere(
|
.andWhere(
|
||||||
(bd) =>
|
(bd) =>
|
||||||
void bd
|
void bd
|
||||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverId`, membershipId)
|
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||||
)
|
)
|
||||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||||
.select(
|
.select(
|
||||||
db.ref("projectId").withSchema(TableName.Environment),
|
db.ref("projectId").withSchema(TableName.Environment),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||||
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerMemberId"),
|
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"),
|
||||||
|
db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||||
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||||
db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||||
db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||||
@@ -201,7 +283,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
),
|
),
|
||||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("approverId").withSchema(TableName.SecretApprovalPolicyApprover)
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
|
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||||
|
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||||
|
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||||
|
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||||
)
|
)
|
||||||
.orderBy("createdAt", "desc");
|
.orderBy("createdAt", "desc");
|
||||||
|
|
||||||
@@ -223,18 +309,26 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath
|
secretPath: el.policySecretPath
|
||||||
|
},
|
||||||
|
committerUser: {
|
||||||
|
userId: el.committerUserId,
|
||||||
|
email: el.committerUserEmail,
|
||||||
|
firstName: el.committerUserFirstName,
|
||||||
|
lastName: el.committerUserLastName,
|
||||||
|
username: el.committerUserUsername
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
key: "reviewerMemberId",
|
key: "reviewerId",
|
||||||
label: "reviewers" as const,
|
label: "reviewers" as const,
|
||||||
mapper: ({ reviewerMemberId: member, reviewerStatus: s }) => (member ? { member, status: s } : undefined)
|
mapper: ({ reviewerUserId, reviewerStatus: s }) =>
|
||||||
|
reviewerUserId ? { userId: reviewerUserId, status: s } : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "approverId",
|
key: "approverUserId",
|
||||||
label: "approvers" as const,
|
label: "approvers" as const,
|
||||||
mapper: ({ approverId }) => approverId
|
mapper: ({ approverUserId }) => approverUserId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "commitId",
|
key: "commitId",
|
||||||
|
|||||||
+24
-30
@@ -87,7 +87,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
await permissionService.getProjectPermission(
|
||||||
actor as ActorType.USER,
|
actor as ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -95,7 +95,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, membership.id);
|
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,19 +113,13 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
}: TListApprovalsDTO) => {
|
}: TListApprovalsDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const { membership } = await permissionService.getProjectPermission(
|
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||||
actor,
|
|
||||||
actorId,
|
|
||||||
projectId,
|
|
||||||
actorAuthMethod,
|
|
||||||
actorOrgId
|
|
||||||
);
|
|
||||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||||
projectId,
|
projectId,
|
||||||
committer,
|
committer,
|
||||||
environment,
|
environment,
|
||||||
status,
|
status,
|
||||||
membershipId: membership.id,
|
userId: actorId,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset
|
||||||
});
|
});
|
||||||
@@ -145,7 +139,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
@@ -154,8 +148,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
throw new UnauthorizedError({ message: "User has no access" });
|
||||||
}
|
}
|
||||||
@@ -180,7 +174,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
@@ -189,8 +183,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
throw new UnauthorizedError({ message: "User has no access" });
|
||||||
}
|
}
|
||||||
@@ -198,7 +192,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const review = await secretApprovalRequestReviewerDAL.findOne(
|
const review = await secretApprovalRequestReviewerDAL.findOne(
|
||||||
{
|
{
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
member: membership.id
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -207,7 +201,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
requestId: secretApprovalRequest.id,
|
requestId: secretApprovalRequest.id,
|
||||||
member: membership.id
|
reviewerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -230,7 +224,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
secretApprovalRequest.projectId,
|
secretApprovalRequest.projectId,
|
||||||
@@ -239,8 +233,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
throw new UnauthorizedError({ message: "User has no access" });
|
||||||
}
|
}
|
||||||
@@ -253,7 +247,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
|
const updatedRequest = await secretApprovalRequestDAL.updateById(secretApprovalRequest.id, {
|
||||||
status,
|
status,
|
||||||
statusChangeBy: membership.id
|
statusChangedByUserId: actorId
|
||||||
});
|
});
|
||||||
return { ...secretApprovalRequest, ...updatedRequest };
|
return { ...secretApprovalRequest, ...updatedRequest };
|
||||||
};
|
};
|
||||||
@@ -270,7 +264,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
if (actor !== ActorType.USER) throw new BadRequestError({ message: "Must be a user" });
|
||||||
|
|
||||||
const { policy, folderId, projectId } = secretApprovalRequest;
|
const { policy, folderId, projectId } = secretApprovalRequest;
|
||||||
const { membership, hasRole } = await permissionService.getProjectPermission(
|
const { hasRole } = await permissionService.getProjectPermission(
|
||||||
ActorType.USER,
|
ActorType.USER,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -280,19 +274,19 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!hasRole(ProjectMembershipRole.Admin) &&
|
!hasRole(ProjectMembershipRole.Admin) &&
|
||||||
secretApprovalRequest.committerId !== membership.id &&
|
secretApprovalRequest.committerUserId !== actorId &&
|
||||||
!policy.approvers.find((approverId) => approverId === membership.id)
|
!policy.approvers.find(({ userId }) => userId === actorId)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedError({ message: "User has no access" });
|
throw new UnauthorizedError({ message: "User has no access" });
|
||||||
}
|
}
|
||||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
||||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status as ApprovalStatus }),
|
(prev, curr) => ({ ...prev, [curr.userId.toString()]: curr.status as ApprovalStatus }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const hasMinApproval =
|
const hasMinApproval =
|
||||||
secretApprovalRequest.policy.approvals <=
|
secretApprovalRequest.policy.approvals <=
|
||||||
secretApprovalRequest.policy.approvers.filter(
|
secretApprovalRequest.policy.approvers.filter(
|
||||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||||
@@ -472,7 +466,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
conflicts: JSON.stringify(conflicts),
|
conflicts: JSON.stringify(conflicts),
|
||||||
hasMerged: true,
|
hasMerged: true,
|
||||||
status: RequestState.Closed,
|
status: RequestState.Closed,
|
||||||
statusChangeBy: membership.id
|
statusChangedByUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -509,7 +503,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
}: TGenerateSecretApprovalRequestDTO) => {
|
}: TGenerateSecretApprovalRequestDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
|
|
||||||
const { permission, membership } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -663,7 +657,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
policyId: policy.id,
|
policyId: policy.id,
|
||||||
status: "open",
|
status: "open",
|
||||||
hasMerged: false,
|
hasMerged: false,
|
||||||
committerId: membership.id
|
committerUserId: actorId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { alphaNumericNanoId } from "@app/lib/nanoid";
|
|||||||
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
|
||||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
|
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
|
||||||
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
|
import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue";
|
||||||
@@ -46,7 +45,6 @@ type TSecretReplicationServiceFactoryDep = {
|
|||||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "find">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "find">;
|
||||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
|
||||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne">;
|
|
||||||
secretApprovalRequestSecretDAL: Pick<
|
secretApprovalRequestSecretDAL: Pick<
|
||||||
TSecretApprovalRequestSecretDALFactory,
|
TSecretApprovalRequestSecretDALFactory,
|
||||||
"insertMany" | "insertApprovalSecretTags"
|
"insertMany" | "insertApprovalSecretTags"
|
||||||
@@ -92,7 +90,6 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
projectMembershipDAL,
|
|
||||||
projectBotService
|
projectBotService
|
||||||
}: TSecretReplicationServiceFactoryDep) => {
|
}: TSecretReplicationServiceFactoryDep) => {
|
||||||
const getReplicatedSecrets = (
|
const getReplicatedSecrets = (
|
||||||
@@ -297,12 +294,6 @@ export const secretReplicationServiceFactory = ({
|
|||||||
);
|
);
|
||||||
// this means it should be a approval request rather than direct replication
|
// this means it should be a approval request rather than direct replication
|
||||||
if (policy && actor === ActorType.USER) {
|
if (policy && actor === ActorType.USER) {
|
||||||
const membership = await projectMembershipDAL.findOne({ projectId, userId: actorId });
|
|
||||||
if (!membership) {
|
|
||||||
logger.error("Project membership not found in %s for user %s", projectId, actorId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id);
|
const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id);
|
||||||
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
|
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
|
||||||
destinationReplicationFolderId,
|
destinationReplicationFolderId,
|
||||||
@@ -316,7 +307,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
policyId: policy.id,
|
policyId: policy.id,
|
||||||
status: "open",
|
status: "open",
|
||||||
hasMerged: false,
|
hasMerged: false,
|
||||||
committerId: membership.id,
|
committerUserId: actorId,
|
||||||
isReplicated: true
|
isReplicated: true
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
|
|||||||
@@ -319,7 +319,6 @@ export const registerRoutes = async (
|
|||||||
auditLogStreamDAL
|
auditLogStreamDAL
|
||||||
});
|
});
|
||||||
const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({
|
const secretApprovalPolicyService = secretApprovalPolicyServiceFactory({
|
||||||
projectMembershipDAL,
|
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -768,7 +767,6 @@ export const registerRoutes = async (
|
|||||||
secretApprovalRequestDAL,
|
secretApprovalRequestDAL,
|
||||||
secretApprovalRequestSecretDAL,
|
secretApprovalRequestSecretDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
projectMembershipDAL,
|
|
||||||
projectBotService
|
projectBotService
|
||||||
});
|
});
|
||||||
const secretRotationQueue = secretRotationQueueFactory({
|
const secretRotationQueue = secretRotationQueueFactory({
|
||||||
|
|||||||
@@ -949,7 +949,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedBy: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@@ -1133,7 +1133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedBy: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@@ -1271,7 +1271,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedBy: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@@ -1397,7 +1397,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedBy: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@@ -1524,7 +1524,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedBy: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
@@ -1638,7 +1638,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
metadata: {
|
metadata: {
|
||||||
committedBy: approval.committerId,
|
committedBy: approval.committerUserId,
|
||||||
secretApprovalRequestId: approval.id,
|
secretApprovalRequestId: approval.id,
|
||||||
secretApprovalRequestSlug: approval.slug
|
secretApprovalRequestSlug: approval.slug
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export const useCreateSecretApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
|
mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approverUserIds,
|
||||||
secretPath,
|
secretPath,
|
||||||
name
|
name
|
||||||
});
|
});
|
||||||
@@ -30,10 +30,10 @@ export const useUpdateSecretApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
|
mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approverUserIds,
|
||||||
secretPath,
|
secretPath,
|
||||||
name
|
name
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export type TSecretApprovalPolicy = {
|
|||||||
envId: string;
|
envId: string;
|
||||||
environment: WorkspaceEnv;
|
environment: WorkspaceEnv;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
approvers: string[];
|
|
||||||
approvals: number;
|
approvals: number;
|
||||||
|
userApprovers: { userId: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretApprovalPoliciesDTO = {
|
export type TGetSecretApprovalPoliciesDTO = {
|
||||||
@@ -26,14 +26,14 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
approvers?: string[];
|
approverUserIds?: string[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateSecretPolicyDTO = {
|
export type TUpdateSecretPolicyDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: string[];
|
approverUserIds?: string[];
|
||||||
secretPath?: string | null;
|
secretPath?: string | null;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
// for invalidating list
|
// for invalidating list
|
||||||
|
|||||||
@@ -47,10 +47,14 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
|||||||
isReplicated?: boolean;
|
isReplicated?: boolean;
|
||||||
slug: string;
|
slug: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
committerId: string;
|
committerUserId: string;
|
||||||
reviewers: {
|
reviewers: {
|
||||||
member: string;
|
userId: string;
|
||||||
status: ApprovalStatus;
|
status: ApprovalStatus;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
}[];
|
}[];
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
@@ -58,8 +62,30 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
|||||||
secretPath: string;
|
secretPath: string;
|
||||||
hasMerged: boolean;
|
hasMerged: boolean;
|
||||||
status: "open" | "close";
|
status: "open" | "close";
|
||||||
policy: TSecretApprovalPolicy;
|
policy: Omit<TSecretApprovalPolicy, "approvers"> & {
|
||||||
statusChangeBy: string;
|
approvers: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
statusChangedByUserId: string;
|
||||||
|
statusChangedByUser?: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
committerUser: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
|
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
|
||||||
commits: ({
|
commits: ({
|
||||||
// if there is no secret means it was creation
|
// if there is no secret means it was creation
|
||||||
|
|||||||
+15
-6
@@ -7,6 +7,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -145,13 +147,20 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
|
|||||||
</TBody>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
<SecretPolicyForm
|
<Modal
|
||||||
workspaceId={workspaceId}
|
|
||||||
isOpen={popUp.secretPolicyForm.isOpen}
|
isOpen={popUp.secretPolicyForm.isOpen}
|
||||||
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||||
members={members}
|
>
|
||||||
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
|
<ModalContent title={popUp.secretPolicyForm.data ? "Edit policy" : "Create policy"}>
|
||||||
/>
|
<SecretPolicyForm
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
isOpen={popUp.secretPolicyForm.isOpen}
|
||||||
|
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||||
|
members={members}
|
||||||
|
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deletePolicy.isOpen}
|
isOpen={popUp.deletePolicy.isOpen}
|
||||||
deleteKey="remove"
|
deleteKey="remove"
|
||||||
|
|||||||
+9
-7
@@ -51,7 +51,7 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id: policy.id,
|
id: policy.id,
|
||||||
approvers: selectedApprovers
|
approverUserIds: selectedApprovers
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@@ -60,7 +60,7 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setSelectedApprovers(policy.approvers);
|
setSelectedApprovers(policy.userApprovers.map(({ userId }) => userId));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -73,7 +73,9 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
isReadOnly
|
||||||
value={policy.approvers?.length ? `${policy.approvers.length} selected` : "None"}
|
value={
|
||||||
|
policy?.userApprovers.length ? `${policy.userApprovers.length} selected` : "None"
|
||||||
|
}
|
||||||
className="text-left"
|
className="text-left"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -84,17 +86,17 @@ export const SecretApprovalPolicyRow = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select members that are allowed to approve changes
|
Select members that are allowed to approve changes
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ id, user }) => {
|
{members?.map(({ user }) => {
|
||||||
const isChecked = selectedApprovers.includes(id);
|
const isChecked = selectedApprovers.includes(user.id);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setSelectedApprovers((state) =>
|
setSelectedApprovers((state) =>
|
||||||
isChecked ? state.filter((el) => el !== id) : [...state, id]
|
isChecked ? state.filter((el) => el !== user.id) : [...state, user.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${id}`}
|
key={`create-policy-members-${user.id}`}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
>
|
>
|
||||||
|
|||||||
+18
-20
@@ -1,4 +1,3 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -15,8 +14,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
@@ -40,9 +37,9 @@ const formSchema = z
|
|||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPath: z.string().optional().nullable(),
|
||||||
approvals: z.number().min(1),
|
approvals: z.number().min(1),
|
||||||
approvers: z.string().array().min(1)
|
approverUserIds: z.string().array().min(1)
|
||||||
})
|
})
|
||||||
.refine((data) => data.approvals <= data.approvers.length, {
|
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||||
path: ["approvals"],
|
path: ["approvals"],
|
||||||
message: "The number of approvals should be lower than the number of approvers."
|
message: "The number of approvals should be lower than the number of approvers."
|
||||||
});
|
});
|
||||||
@@ -50,7 +47,6 @@ const formSchema = z
|
|||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const SecretPolicyForm = ({
|
export const SecretPolicyForm = ({
|
||||||
isOpen,
|
|
||||||
onToggle,
|
onToggle,
|
||||||
members = [],
|
members = [],
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -59,20 +55,22 @@ export const SecretPolicyForm = ({
|
|||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
|
||||||
watch,
|
watch,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
|
values: editValues
|
||||||
|
? {
|
||||||
|
...editValues,
|
||||||
|
approverUserIds: editValues.userApprovers.map(({ userId }) => userId),
|
||||||
|
environment: editValues.environment.slug
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
});
|
});
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const selectedEnvironment = watch("environment");
|
const selectedEnvironment = watch("environment");
|
||||||
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
const environments = currentWorkspace?.environments || [];
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) reset({});
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const isEditMode = Boolean(editValues);
|
const isEditMode = Boolean(editValues);
|
||||||
|
|
||||||
@@ -131,8 +129,6 @@ export const SecretPolicyForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
|
||||||
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
|
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -155,6 +151,7 @@ export const SecretPolicyForm = ({
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
isDisabled={isEditMode}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={(val) => onChange(val)}
|
onValueChange={(val) => onChange(val)}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
@@ -186,7 +183,7 @@ export const SecretPolicyForm = ({
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="approvers"
|
name="approverUserIds"
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Approvers Required"
|
label="Approvers Required"
|
||||||
@@ -208,17 +205,19 @@ export const SecretPolicyForm = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select members that are allowed to approve changes
|
Select members that are allowed to approve changes
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members.map(({ id, user }) => {
|
{members.map(({ user }) => {
|
||||||
const isChecked = value?.includes(id);
|
const isChecked = value?.includes(user.id);
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
onChange(
|
onChange(
|
||||||
isChecked ? value?.filter((el) => el !== id) : [...(value || []), id]
|
isChecked
|
||||||
|
? value?.filter((el) => el !== user.id)
|
||||||
|
: [...(value || []), user.id]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${id}`}
|
key={`create-policy-members-${user.id}`}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
>
|
>
|
||||||
@@ -258,7 +257,6 @@ export const SecretPolicyForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-24
@@ -31,7 +31,7 @@ import {
|
|||||||
useGetSecretApprovalRequests,
|
useGetSecretApprovalRequests,
|
||||||
useGetWorkspaceUsers
|
useGetWorkspaceUsers
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types";
|
import { ApprovalStatus, TSecretApprovalRequest } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
generateCommitText,
|
generateCommitText,
|
||||||
@@ -63,14 +63,9 @@ export const SecretApprovalRequest = () => {
|
|||||||
});
|
});
|
||||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||||
useGetSecretApprovalRequestCount({ workspaceId });
|
useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { user: presentUser } = useUser();
|
const { user: userSession } = useUser();
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||||
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
|
||||||
(prev, curr) => ({ ...prev, [curr.id]: curr }),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
|
|
||||||
const isSecretApprovalScreen = Boolean(selectedApproval);
|
const isSecretApprovalScreen = Boolean(selectedApproval);
|
||||||
|
|
||||||
const handleGoBackSecretRequestDetail = () => {
|
const handleGoBackSecretRequestDetail = () => {
|
||||||
@@ -93,10 +88,8 @@ export const SecretApprovalRequest = () => {
|
|||||||
>
|
>
|
||||||
<SecretApprovalRequestChanges
|
<SecretApprovalRequestChanges
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
members={membersGroupById}
|
|
||||||
approvalRequestId={selectedApproval?.id || ""}
|
approvalRequestId={selectedApproval?.id || ""}
|
||||||
onGoBack={handleGoBackSecretRequestDetail}
|
onGoBack={handleGoBackSecretRequestDetail}
|
||||||
committer={membersGroupById?.[selectedApproval?.committerId || ""]}
|
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
@@ -182,10 +175,12 @@ export const SecretApprovalRequest = () => {
|
|||||||
{members?.map(({ user, id }) => (
|
{members?.map(({ user, id }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCommitterFilter((state) => (state === id ? undefined : id))
|
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
|
||||||
}
|
}
|
||||||
key={`request-filter-member-${id}`}
|
key={`request-filter-member-${id}`}
|
||||||
icon={committerFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
|
icon={
|
||||||
|
committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
|
||||||
|
}
|
||||||
iconPos="right"
|
iconPos="right"
|
||||||
>
|
>
|
||||||
{user.username}
|
{user.username}
|
||||||
@@ -208,19 +203,16 @@ export const SecretApprovalRequest = () => {
|
|||||||
const {
|
const {
|
||||||
id: reqId,
|
id: reqId,
|
||||||
commits,
|
commits,
|
||||||
committerId,
|
|
||||||
createdAt,
|
createdAt,
|
||||||
policy,
|
|
||||||
reviewers,
|
reviewers,
|
||||||
status,
|
status,
|
||||||
|
committerUser,
|
||||||
isReplicated: isReplication
|
isReplicated: isReplication
|
||||||
} = secretApproval;
|
} = secretApproval;
|
||||||
const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1;
|
const isReviewed = reviewers.some(
|
||||||
const isReviewed =
|
({ status: reviewStatus, userId }) =>
|
||||||
reviewers.findIndex(
|
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
|
||||||
({ member, status: reviewStatus }) =>
|
);
|
||||||
member === myMembershipId && reviewStatus === ApprovalStatus.APPROVED
|
|
||||||
) !== -1;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={reqId}
|
key={reqId}
|
||||||
@@ -239,11 +231,9 @@ export const SecretApprovalRequest = () => {
|
|||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||||
{membersGroupById?.[committerId]?.user?.firstName}{" "}
|
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||||
{membersGroupById?.[committerId]?.user?.lastName} (
|
{committerUser?.email}){isReplication && " via replication"}
|
||||||
{membersGroupById?.[committerId]?.user?.email})
|
{!isReviewed && status === "open" && " - Review required"}
|
||||||
{isReplication && " via replication"}
|
|
||||||
{isApprover && !isReviewed && status === "open" && " - Review required"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ type Props = {
|
|||||||
status: "close" | "open";
|
status: "close" | "open";
|
||||||
approvals: number;
|
approvals: number;
|
||||||
canApprove?: boolean;
|
canApprove?: boolean;
|
||||||
statusChangeByEmail: string;
|
statusChangeByEmail?: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+30
-33
@@ -19,7 +19,7 @@ import {
|
|||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
useUpdateSecretApprovalReviewStatus
|
useUpdateSecretApprovalReviewStatus
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
|
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
|
||||||
import { formatReservedPaths } from "@app/lib/fn/string";
|
import { formatReservedPaths } from "@app/lib/fn/string";
|
||||||
|
|
||||||
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
||||||
@@ -73,18 +73,14 @@ type Props = {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
approvalRequestId: string;
|
approvalRequestId: string;
|
||||||
onGoBack: () => void;
|
onGoBack: () => void;
|
||||||
committer?: TWorkspaceUser;
|
|
||||||
members?: Record<string, TWorkspaceUser>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretApprovalRequestChanges = ({
|
export const SecretApprovalRequestChanges = ({
|
||||||
approvalRequestId,
|
approvalRequestId,
|
||||||
onGoBack,
|
onGoBack,
|
||||||
committer,
|
workspaceId
|
||||||
workspaceId,
|
|
||||||
members = {}
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { user } = useUser();
|
const { user: userSession } = useUser();
|
||||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||||
const {
|
const {
|
||||||
data: secretApprovalRequestDetails,
|
data: secretApprovalRequestDetails,
|
||||||
@@ -105,22 +101,20 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
||||||
|
|
||||||
// membership of present user
|
// membership of present user
|
||||||
const myMembership = Object.values(members).find(
|
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||||
({ user: membershipUser }) => membershipUser.email === user.email
|
({ userId }) => userId === userSession.id
|
||||||
);
|
);
|
||||||
const myMembershipId = myMembership?.id || "";
|
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||||
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.includes(myMembershipId);
|
|
||||||
const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce<
|
|
||||||
Record<string, ApprovalStatus>
|
Record<string, ApprovalStatus>
|
||||||
>(
|
>(
|
||||||
(prev, curr) => ({
|
(prev, curr) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[curr.member]: curr.status
|
[curr.userId]: curr.status
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const hasApproved = reviewedMembers?.[myMembershipId] === ApprovalStatus.APPROVED;
|
const hasApproved = reviewedUsers?.[userSession.id] === ApprovalStatus.APPROVED;
|
||||||
const hasRejected = reviewedMembers?.[myMembershipId] === ApprovalStatus.REJECTED;
|
const hasRejected = reviewedUsers?.[userSession.id] === ApprovalStatus.REJECTED;
|
||||||
|
|
||||||
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
||||||
try {
|
try {
|
||||||
@@ -159,7 +153,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
const isMergable =
|
const isMergable =
|
||||||
secretApprovalRequestDetails?.policy?.approvals <=
|
secretApprovalRequestDetails?.policy?.approvals <=
|
||||||
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
||||||
(approverId) => reviewedMembers?.[approverId] === ApprovalStatus.APPROVED
|
({ userId }) => reviewedUsers?.[userId] === ApprovalStatus.APPROVED
|
||||||
).length;
|
).length;
|
||||||
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
||||||
|
|
||||||
@@ -191,8 +185,9 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm text-bunker-300">
|
<div className="flex items-center text-sm text-bunker-300">
|
||||||
{committer?.user?.firstName}
|
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
||||||
{committer?.user?.lastName} ({committer?.user?.email}) wants to change{" "}
|
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
|
||||||
|
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
|
||||||
{secretApprovalRequestDetails.commits.length} secret values in
|
{secretApprovalRequestDetails.commits.length} secret values in
|
||||||
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
|
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
|
||||||
{secretApprovalRequestDetails.environment}
|
{secretApprovalRequestDetails.environment}
|
||||||
@@ -256,9 +251,7 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
approvals={secretApprovalRequestDetails.policy.approvals || 0}
|
approvals={secretApprovalRequestDetails.policy.approvals || 0}
|
||||||
status={secretApprovalRequestDetails.status}
|
status={secretApprovalRequestDetails.status}
|
||||||
isMergable={isMergable}
|
isMergable={isMergable}
|
||||||
statusChangeByEmail={
|
statusChangeByEmail={secretApprovalRequestDetails.statusChangedByUser?.email}
|
||||||
members[secretApprovalRequestDetails?.statusChangeBy || ""]?.user?.email || ""
|
|
||||||
}
|
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,17 +259,19 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
||||||
<div className="text-sm text-bunker-300">Reviewers</div>
|
<div className="text-sm text-bunker-300">Reviewers</div>
|
||||||
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
||||||
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApproverId) => {
|
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
||||||
const userDetails = members?.[requiredApproverId]?.user;
|
const status = reviewedUsers?.[requiredApprover.userId];
|
||||||
const status = reviewedMembers?.[requiredApproverId];
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
key={`required-approver-${requiredApproverId}`}
|
key={`required-approver-${requiredApprover.userId}`}
|
||||||
>
|
>
|
||||||
<div className="flex-grow text-sm">
|
<div className="flex-grow text-sm">
|
||||||
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
|
<Tooltip
|
||||||
<span>{userDetails?.email} </span>
|
content={`${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{requiredApprover?.email} </span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,19 +285,21 @@ export const SecretApprovalRequestChanges = ({
|
|||||||
})}
|
})}
|
||||||
{secretApprovalRequestDetails?.reviewers
|
{secretApprovalRequestDetails?.reviewers
|
||||||
.filter(
|
.filter(
|
||||||
({ member }) => !secretApprovalRequestDetails?.policy?.approvers?.includes(member)
|
(reviewer) =>
|
||||||
|
!secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||||
|
({ userId }) => userId === reviewer.userId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.map((reviewer) => {
|
.map((reviewer) => {
|
||||||
const userDetails = members?.[reviewer.member]?.user;
|
const status = reviewedUsers?.[reviewer.userId];
|
||||||
const status = reviewedMembers?.[reviewer.status];
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||||
key={`required-approver-${reviewer.member}`}
|
key={`required-approver-${reviewer.userId}`}
|
||||||
>
|
>
|
||||||
<div className="flex-grow text-sm">
|
<div className="flex-grow text-sm">
|
||||||
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
|
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
|
||||||
<span>{userDetails?.email} </span>
|
<span>{reviewer?.email} </span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="text-red">*</span>
|
<span className="text-red">*</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user