mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-11 12:11:38 +00:00
Compare commits
5 Commits
create-pol
...
daniel/app
Author | SHA1 | Date | |
---|---|---|---|
31381b9b4b | |||
9736bc517d | |||
0aaad1eeb8 | |||
8b2adbbe95 | |||
a96cbe6252 |
@ -0,0 +1,91 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
// ? ACCESS APPROVALS
|
||||||
|
const accessApprovalPolicyHasSecretPathColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicy,
|
||||||
|
"secretPath"
|
||||||
|
);
|
||||||
|
const accessApprovalPolicyHasNewSecretPathsColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicy,
|
||||||
|
"secretPaths"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||||
|
if (!accessApprovalPolicyHasNewSecretPathsColumn) {
|
||||||
|
t.jsonb("secretPaths").notNullable().defaultTo("[]");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (accessApprovalPolicyHasSecretPathColumn) {
|
||||||
|
// Move the existing secretPath values to the new secretPaths column
|
||||||
|
await knex(TableName.AccessApprovalPolicy)
|
||||||
|
.select("id", "secretPath")
|
||||||
|
.whereNotNull("secretPath")
|
||||||
|
.whereNot("secretPath", "")
|
||||||
|
.update({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore -- secretPaths are not in the type definition yet
|
||||||
|
secretPaths: knex.raw("to_jsonb(ARRAY[??])", ["secretPath"])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO(daniel): Drop the secretPath column in the future when this has stabilized
|
||||||
|
|
||||||
|
// ? SECRET CHANGE APPROVALS
|
||||||
|
const secretChangeApprovalPolicyHasSecretPathColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalPolicy,
|
||||||
|
"secretPath"
|
||||||
|
);
|
||||||
|
const secretChangeApprovalPolicyHasNewSecretPathsColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalPolicy,
|
||||||
|
"secretPaths"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
if (!secretChangeApprovalPolicyHasNewSecretPathsColumn) {
|
||||||
|
t.jsonb("secretPaths").notNullable().defaultTo("[]");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (secretChangeApprovalPolicyHasSecretPathColumn) {
|
||||||
|
// Move the existing secretPath values to the new secretPaths column
|
||||||
|
await knex(TableName.SecretApprovalPolicy)
|
||||||
|
.select("id", "secretPath")
|
||||||
|
.whereNotNull("secretPath")
|
||||||
|
.whereNot("secretPath", "")
|
||||||
|
.update({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore -- secretPaths are not in the type definition yet
|
||||||
|
secretPaths: knex.raw("to_jsonb(ARRAY[??])", ["secretPath"])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(daniel): Drop the secretPath column in the future when this has stabilized.
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
// TODO(daniel): Restore the secretPath columns when we add dropping in the up migration. (needs to be re-filled with data from the `secretPaths` column)
|
||||||
|
|
||||||
|
const accessApprovalPolicyHasNewSecretsPathsColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.AccessApprovalPolicy,
|
||||||
|
"secretPaths"
|
||||||
|
);
|
||||||
|
const secretChangeApprovalPolicyHasSecretPathColumn = await knex.schema.hasColumn(
|
||||||
|
TableName.SecretApprovalPolicy,
|
||||||
|
"secretPaths"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||||
|
if (accessApprovalPolicyHasNewSecretsPathsColumn) {
|
||||||
|
t.dropColumn("secretPaths");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||||
|
if (secretChangeApprovalPolicyHasSecretPathColumn) {
|
||||||
|
t.dropColumn("secretPaths");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -15,7 +15,8 @@ export const AccessApprovalPoliciesSchema = z.object({
|
|||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
enforcementLevel: z.string().default("hard")
|
enforcementLevel: z.string().default("hard"),
|
||||||
|
secretPaths: z.unknown()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||||
|
@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const IdentityMetadataSchema = z.object({
|
export const IdentityMetadataSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
value: z.string(),
|
value: z.string().nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
identityId: z.string().uuid().nullable().optional(),
|
identityId: z.string().uuid().nullable().optional(),
|
||||||
|
@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const KmsRootConfigSchema = z.object({
|
export const KmsRootConfigSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedRootKey: zodBuffer,
|
encryptedRootKey: zodBuffer,
|
||||||
encryptionStrategy: z.string(),
|
encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,8 @@ export const ProjectUserAdditionalPrivilegeSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
projectId: z.string()
|
projectId: z.string(),
|
||||||
|
accessRequestId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;
|
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;
|
||||||
|
@ -15,7 +15,8 @@ export const SecretApprovalPoliciesSchema = z.object({
|
|||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
enforcementLevel: z.string().default("hard")
|
enforcementLevel: z.string().default("hard"),
|
||||||
|
secretPaths: z.unknown()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||||
|
@ -2,6 +2,7 @@ import { nanoid } from "nanoid";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
|
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
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";
|
||||||
@ -19,7 +20,10 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
projectSlug: z.string().trim(),
|
projectSlug: z.string().trim(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().trim().default("/"),
|
secretPaths: z
|
||||||
|
.string()
|
||||||
|
.array()
|
||||||
|
.transform((val) => val.map((v) => prefixWithSlash(removeTrailingSlash(v)).trim())),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
approvers: z
|
approvers: z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
@ -49,6 +53,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
|
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
|
||||||
enforcementLevel: req.body.enforcementLevel
|
enforcementLevel: req.body.enforcementLevel
|
||||||
});
|
});
|
||||||
|
|
||||||
return { approval };
|
return { approval };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -134,11 +139,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z
|
secretPaths: z.string().array().optional(),
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.optional()
|
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
|
||||||
approvers: z
|
approvers: z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||||
|
@ -20,7 +20,14 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
permissions: z.any().array(),
|
requestedActions: z.object({
|
||||||
|
read: z.boolean(),
|
||||||
|
edit: z.boolean(),
|
||||||
|
create: z.boolean(),
|
||||||
|
delete: z.boolean()
|
||||||
|
}),
|
||||||
|
environment: z.string(),
|
||||||
|
secretPaths: z.string().array(),
|
||||||
isTemporary: z.boolean(),
|
isTemporary: z.boolean(),
|
||||||
temporaryRange: z.string().optional()
|
temporaryRange: z.string().optional()
|
||||||
}),
|
}),
|
||||||
@ -39,7 +46,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
permissions: req.body.permissions,
|
environment: req.body.environment,
|
||||||
|
secretPaths: req.body.secretPaths,
|
||||||
|
requestedActions: req.body.requestedActions,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectSlug: req.query.projectSlug,
|
projectSlug: req.query.projectSlug,
|
||||||
temporaryRange: req.body.temporaryRange,
|
temporaryRange: req.body.temporaryRange,
|
||||||
@ -107,7 +116,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: z.string().array(),
|
approvers: z.string().array(),
|
||||||
secretPath: z.string().nullish(),
|
secretPaths: z.string().array(),
|
||||||
envId: z.string(),
|
envId: z.string(),
|
||||||
enforcementLevel: z.string()
|
enforcementLevel: z.string()
|
||||||
}),
|
}),
|
||||||
|
@ -2,7 +2,7 @@ import { nanoid } from "nanoid";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
import { ApproverType } from "@app/ee/services/access-approval-policy/access-approval-policy-types";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { EnforcementLevel } from "@app/lib/types";
|
import { EnforcementLevel } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
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";
|
||||||
@ -21,12 +21,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
workspaceId: z.string(),
|
workspaceId: z.string(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
secretPath: z
|
secretPaths: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.array()
|
||||||
.nullable()
|
.transform((val) => val.map((v) => prefixWithSlash(removeTrailingSlash(v)).trim())),
|
||||||
.default("/")
|
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
|
||||||
approvers: z
|
approvers: z
|
||||||
.discriminatedUnion("type", [
|
.discriminatedUnion("type", [
|
||||||
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
z.object({ type: z.literal(ApproverType.Group), id: z.string() }),
|
||||||
@ -55,6 +53,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
|
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
|
||||||
enforcementLevel: req.body.enforcementLevel
|
enforcementLevel: req.body.enforcementLevel
|
||||||
});
|
});
|
||||||
|
|
||||||
return { approval };
|
return { approval };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -79,12 +78,12 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
|||||||
.array()
|
.array()
|
||||||
.min(1, { message: "At least one approver should be provided" }),
|
.min(1, { message: "At least one approver should be provided" }),
|
||||||
approvals: z.number().min(1).default(1),
|
approvals: z.number().min(1).default(1),
|
||||||
secretPath: z
|
secretPaths: z
|
||||||
.string()
|
.string()
|
||||||
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.transform((val) => (val ? val.map((v) => prefixWithSlash(removeTrailingSlash(v)).trim()) : val)),
|
||||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
|
||||||
.transform((val) => (val === "" ? "/" : val)),
|
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
@ -41,8 +41,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
approvals: SecretApprovalRequestsSchema.extend({
|
approvals: SecretApprovalRequestsSchema.extend({
|
||||||
// secretPath: z.string(),
|
|
||||||
policy: z.object({
|
policy: z.object({
|
||||||
|
secretPaths: z.string().array(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
@ -51,7 +51,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
userId: z.string().nullable().optional()
|
userId: z.string().nullable().optional()
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
|
||||||
enforcementLevel: z.string()
|
enforcementLevel: z.string()
|
||||||
}),
|
}),
|
||||||
committerUser: approvalRequestUser,
|
committerUser: approvalRequestUser,
|
||||||
@ -253,13 +252,12 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
approval: SecretApprovalRequestsSchema.merge(
|
approval: SecretApprovalRequestsSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
// secretPath: z.string(),
|
|
||||||
policy: z.object({
|
policy: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
approvals: z.number(),
|
approvals: z.number(),
|
||||||
approvers: approvalRequestUser.array(),
|
approvers: approvalRequestUser.array(),
|
||||||
secretPath: z.string().optional().nullable(),
|
secretPaths: z.string().array(),
|
||||||
enforcementLevel: z.string()
|
enforcementLevel: z.string()
|
||||||
}),
|
}),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
@ -308,6 +306,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
id: req.params.id
|
id: req.params.id
|
||||||
});
|
});
|
||||||
|
|
||||||
return { approval };
|
return { approval };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -17,15 +17,21 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
||||||
customFilter?: {
|
customFilter?: {
|
||||||
policyId?: string;
|
policyId?: string;
|
||||||
|
secretPaths?: string[];
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const result = await tx(TableName.AccessApprovalPolicy)
|
const query = tx(TableName.AccessApprovalPolicy)
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
.where(buildFindFilter(filter))
|
.where(buildFindFilter(filter))
|
||||||
.where((qb) => {
|
.where((qb) => {
|
||||||
if (customFilter?.policyId) {
|
if (customFilter?.policyId) {
|
||||||
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
|
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
|
||||||
}
|
}
|
||||||
|
if (customFilter?.secretPaths) {
|
||||||
|
void qb.whereRaw(`${TableName.AccessApprovalPolicy}.secretPaths @> ?::jsonb`, [
|
||||||
|
JSON.stringify(customFilter.secretPaths)
|
||||||
|
]);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
@ -43,6 +49,51 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||||
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
|
.select(selectAllTableCols(TableName.AccessApprovalPolicy));
|
||||||
|
|
||||||
|
const result = await query;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessApprovalPolicyFindOneQuery = async (
|
||||||
|
tx: Knex,
|
||||||
|
filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>,
|
||||||
|
customFilter?: {
|
||||||
|
policyId?: string;
|
||||||
|
secretPaths?: string[];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const query = tx(TableName.AccessApprovalPolicy)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
.where(buildFindFilter(filter))
|
||||||
|
.where((qb) => {
|
||||||
|
if (customFilter?.policyId) {
|
||||||
|
void qb.where(`${TableName.AccessApprovalPolicy}.id`, "=", customFilter.policyId);
|
||||||
|
}
|
||||||
|
if (customFilter?.secretPaths) {
|
||||||
|
void qb.whereRaw(`${TableName.AccessApprovalPolicy}."secretPaths" = ?::jsonb`, [
|
||||||
|
JSON.stringify(customFilter.secretPaths)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.AccessApprovalPolicyApprover,
|
||||||
|
`${TableName.AccessApprovalPolicy}.id`,
|
||||||
|
`${TableName.AccessApprovalPolicyApprover}.policyId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.AccessApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
|
||||||
|
.select(tx.ref("username").withSchema(TableName.Users).as("approverUsername"))
|
||||||
|
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(tx.ref("approverGroupId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||||
|
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||||
|
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||||
|
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||||
|
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||||
|
.select(selectAllTableCols(TableName.AccessApprovalPolicy))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const result = await query;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,8 +160,8 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
slug: data.envSlug
|
slug: data.envSlug
|
||||||
},
|
},
|
||||||
projectId: data.projectId,
|
projectId: data.projectId,
|
||||||
...AccessApprovalPoliciesSchema.parse(data)
|
...AccessApprovalPoliciesSchema.parse(data),
|
||||||
// secretPath: data.secretPath || undefined,
|
secretPaths: data.secretPaths as string[]
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
@ -139,5 +190,52 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...accessApprovalPolicyOrm, find, findById };
|
const findOne = async (
|
||||||
|
filter: Partial<Omit<TAccessApprovalPolicies, "secretPaths">>,
|
||||||
|
customFilter?: { secretPaths?: string[] },
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
const doc = await accessApprovalPolicyFindOneQuery(tx || db.replicaNode(), filter, customFilter);
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [formattedDoc] = sqlNestRelationships({
|
||||||
|
data: [doc],
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (data) => ({
|
||||||
|
environment: {
|
||||||
|
id: data.envId,
|
||||||
|
name: data.envName,
|
||||||
|
slug: data.envSlug
|
||||||
|
},
|
||||||
|
projectId: data.projectId,
|
||||||
|
...AccessApprovalPoliciesSchema.parse(data)
|
||||||
|
}),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "approverUserId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverUserId: id, approverUsername }) => ({
|
||||||
|
id,
|
||||||
|
type: ApproverType.User,
|
||||||
|
name: approverUsername
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "approverGroupId",
|
||||||
|
label: "approvers" as const,
|
||||||
|
mapper: ({ approverGroupId: id }) => ({
|
||||||
|
id,
|
||||||
|
type: ApproverType.Group
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedDoc;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...accessApprovalPolicyOrm, find, findById, findOne };
|
||||||
};
|
};
|
||||||
|
@ -48,7 +48,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
secretPath,
|
secretPaths,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
@ -138,7 +138,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
{
|
{
|
||||||
envId: env.id,
|
envId: env.id,
|
||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPaths: JSON.stringify(secretPaths),
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
},
|
},
|
||||||
@ -166,7 +166,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
return { ...accessApproval, environment: env, projectId: project.id };
|
return { ...accessApproval, environment: env, projectId: project.id, secretPaths };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAccessApprovalPolicyByProjectSlug = async ({
|
const getAccessApprovalPolicyByProjectSlug = async ({
|
||||||
@ -190,13 +190,14 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||||
|
|
||||||
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id });
|
||||||
|
|
||||||
return accessApprovalPolicies;
|
return accessApprovalPolicies;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAccessApprovalPolicy = async ({
|
const updateAccessApprovalPolicy = async ({
|
||||||
policyId,
|
policyId,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPaths,
|
||||||
name,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@ -246,7 +247,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
accessApprovalPolicy.id,
|
accessApprovalPolicy.id,
|
||||||
{
|
{
|
||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPaths: JSON.stringify(secretPaths),
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
},
|
},
|
||||||
@ -321,13 +322,14 @@ export const accessApprovalPolicyServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
ProjectPermissionActions.Delete,
|
ProjectPermissionActions.Delete,
|
||||||
ProjectPermissionSub.SecretApproval
|
ProjectPermissionSub.SecretApproval
|
||||||
);
|
);
|
||||||
|
|
||||||
await accessApprovalPolicyDAL.deleteById(policyId);
|
await accessApprovalPolicyDAL.deleteById(policyId);
|
||||||
return policy;
|
return { ...policy, secretPaths: policy.secretPaths as string[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAccessPolicyCountByEnvSlug = async ({
|
const getAccessPolicyCountByEnvSlug = async ({
|
||||||
|
@ -20,7 +20,7 @@ export enum ApproverType {
|
|||||||
|
|
||||||
export type TCreateAccessApprovalPolicy = {
|
export type TCreateAccessApprovalPolicy = {
|
||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath: string;
|
secretPaths: string[];
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
@ -32,7 +32,7 @@ export type TUpdateAccessApprovalPolicy = {
|
|||||||
policyId: string;
|
policyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||||
secretPath?: string;
|
secretPaths?: string[];
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@ -59,7 +59,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
|
||||||
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||||
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
db.ref("secretPaths").withSchema(TableName.AccessApprovalPolicy).as("policySecretPaths"),
|
||||||
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
||||||
)
|
)
|
||||||
@ -78,7 +78,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
|
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: ADD SUPPORT FOR GROUPS!!!!
|
|
||||||
.select(
|
.select(
|
||||||
db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
|
||||||
db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
|
||||||
@ -116,9 +115,9 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
id: doc.policyId,
|
id: doc.policyId,
|
||||||
name: doc.policyName,
|
name: doc.policyName,
|
||||||
approvals: doc.policyApprovals,
|
approvals: doc.policyApprovals,
|
||||||
secretPath: doc.policySecretPath,
|
|
||||||
enforcementLevel: doc.policyEnforcementLevel,
|
enforcementLevel: doc.policyEnforcementLevel,
|
||||||
envId: doc.policyEnvId
|
envId: doc.policyEnvId,
|
||||||
|
secretPaths: doc.policySecretPaths as string[]
|
||||||
},
|
},
|
||||||
requestedByUser: {
|
requestedByUser: {
|
||||||
userId: doc.requestedByUserId,
|
userId: doc.requestedByUserId,
|
||||||
@ -250,7 +249,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).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.AccessApprovalPolicy).as("policySecretPath"),
|
tx.ref("secretPaths").withSchema(TableName.AccessApprovalPolicy).as("policySecretPaths"),
|
||||||
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals")
|
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals")
|
||||||
);
|
);
|
||||||
@ -270,8 +269,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
id: el.policyId,
|
id: el.policyId,
|
||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
enforcementLevel: el.policyEnforcementLevel
|
secretPaths: el.policySecretPaths as string[]
|
||||||
},
|
},
|
||||||
requestedByUser: {
|
requestedByUser: {
|
||||||
userId: el.requestedByUserId,
|
userId: el.requestedByUserId,
|
||||||
|
@ -4,11 +4,7 @@ import { BadRequestError } from "@app/lib/errors";
|
|||||||
|
|
||||||
import { TVerifyPermission } from "./access-approval-request-types";
|
import { TVerifyPermission } from "./access-approval-request-types";
|
||||||
|
|
||||||
function filterUnique(value: string, index: number, array: string[]) {
|
export const verifyRequestedPermissions = ({ permissions, checkPath }: TVerifyPermission) => {
|
||||||
return array.indexOf(value) === index;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) => {
|
|
||||||
const permission = unpackRules(
|
const permission = unpackRules(
|
||||||
permissions as PackRule<{
|
permissions as PackRule<{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -22,32 +18,20 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
|
|||||||
throw new BadRequestError({ message: "No permission provided" });
|
throw new BadRequestError({ message: "No permission provided" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestedPermissions: string[] = [];
|
for (const perm of permission) {
|
||||||
|
|
||||||
for (const p of permission) {
|
|
||||||
if (p.action[0] === "read") requestedPermissions.push("Read Access");
|
|
||||||
if (p.action[0] === "create") requestedPermissions.push("Create Access");
|
|
||||||
if (p.action[0] === "delete") requestedPermissions.push("Delete Access");
|
|
||||||
if (p.action[0] === "edit") requestedPermissions.push("Edit Access");
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstPermission = permission[0];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
||||||
const permissionSecretPath = firstPermission.conditions?.secretPath?.$glob;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment
|
||||||
const permissionEnv = firstPermission.conditions?.environment;
|
const permissionEnv = perm.conditions?.environment;
|
||||||
|
|
||||||
if (!permissionEnv || typeof permissionEnv !== "string") {
|
if (!permissionEnv || typeof permissionEnv !== "string") {
|
||||||
throw new BadRequestError({ message: "Permission environment is not a string" });
|
throw new BadRequestError({ message: "Permission environment is not a string" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (checkPath) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||||
|
const permissionSecretPath = perm.conditions?.secretPath?.$glob;
|
||||||
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
if (!permissionSecretPath || typeof permissionSecretPath !== "string") {
|
||||||
throw new BadRequestError({ message: "Permission path is not a string" });
|
throw new BadRequestError({ message: "Permission path is not a string" });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return {
|
}
|
||||||
envSlug: permissionEnv,
|
|
||||||
secretPath: permissionSecretPath,
|
|
||||||
accessTypes: requestedPermissions.filter(filterUnique)
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { packRules } from "@casl/ability/extra";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-poli
|
|||||||
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal";
|
||||||
import { TGroupDALFactory } from "../group/group-dal";
|
import { TGroupDALFactory } from "../group/group-dal";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
|
||||||
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
|
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
|
||||||
@ -89,7 +91,9 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
isTemporary,
|
isTemporary,
|
||||||
temporaryRange,
|
temporaryRange,
|
||||||
actorId,
|
actorId,
|
||||||
permissions: requestedPermissions,
|
environment: envSlug,
|
||||||
|
secretPaths,
|
||||||
|
requestedActions,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
@ -116,18 +120,75 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
|
|
||||||
await projectDAL.checkProjectUpgradeStatus(project.id);
|
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||||
|
|
||||||
const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions });
|
const requestedPermissions: unknown[] = [];
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{ action: ProjectPermissionActions.Read, allowed: requestedActions.read },
|
||||||
|
{ action: ProjectPermissionActions.Create, allowed: requestedActions.create },
|
||||||
|
{ action: ProjectPermissionActions.Delete, allowed: requestedActions.delete },
|
||||||
|
{ action: ProjectPermissionActions.Edit, allowed: requestedActions.edit }
|
||||||
|
];
|
||||||
|
|
||||||
|
const enabledActions = actions
|
||||||
|
.filter(({ allowed }) => allowed)
|
||||||
|
.map(({ action }) => action[0].toUpperCase() + action.slice(1));
|
||||||
|
|
||||||
|
if (secretPaths.length) {
|
||||||
|
for (const secretPath of secretPaths) {
|
||||||
|
const permission = packRules(
|
||||||
|
actions
|
||||||
|
.filter(({ allowed }) => allowed)
|
||||||
|
.map(({ action }) => ({
|
||||||
|
action,
|
||||||
|
subject: [ProjectPermissionSub.Secrets],
|
||||||
|
conditions: {
|
||||||
|
environment: envSlug,
|
||||||
|
secretPath: {
|
||||||
|
$glob: secretPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyRequestedPermissions({ permissions: permission });
|
||||||
|
requestedPermissions.push(...permission);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const permission = packRules(
|
||||||
|
actions
|
||||||
|
.filter(({ allowed }) => allowed)
|
||||||
|
.map(({ action }) => ({
|
||||||
|
action,
|
||||||
|
subject: [ProjectPermissionSub.Secrets],
|
||||||
|
conditions: {
|
||||||
|
environment: envSlug
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// We disable path checking here as there will be no path to check (full environment access)
|
||||||
|
verifyRequestedPermissions({ permissions: permission, checkPath: false });
|
||||||
|
requestedPermissions.push(...permission);
|
||||||
|
}
|
||||||
|
|
||||||
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
|
||||||
|
|
||||||
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
|
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
|
||||||
|
|
||||||
const policy = await accessApprovalPolicyDAL.findOne({
|
const policy = await accessApprovalPolicyDAL.findOne(
|
||||||
envId: environment.id,
|
{
|
||||||
secretPath
|
envId: environment.id
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
secretPaths: secretPaths?.length ? secretPaths : []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!policy) {
|
if (!policy) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: `No policy in environment with slug '${environment.slug}' and with secret path '${secretPath}' was found.`
|
message: `No policy in environment with slug '${environment.slug}' and with secret paths '${secretPaths?.join(
|
||||||
|
", "
|
||||||
|
)}' was found.`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,9 +285,9 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
requesterFullName,
|
requesterFullName,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
requesterEmail: requestedByUser.email as string,
|
requesterEmail: requestedByUser.email as string,
|
||||||
secretPath,
|
secretPath: secretPaths?.join(", ") || "",
|
||||||
environment: envSlug,
|
environment: envSlug,
|
||||||
permissions: accessTypes,
|
permissions: enabledActions,
|
||||||
approvalUrl
|
approvalUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,9 +305,9 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
...(isTemporary && {
|
...(isTemporary && {
|
||||||
expiresIn: ms(ms(temporaryRange || ""), { long: true })
|
expiresIn: ms(ms(temporaryRange || ""), { long: true })
|
||||||
}),
|
}),
|
||||||
secretPath,
|
secretPath: secretPaths?.join(", ") || "",
|
||||||
environment: envSlug,
|
environment: envSlug,
|
||||||
permissions: accessTypes,
|
permissions: enabledActions,
|
||||||
approvalUrl
|
approvalUrl
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.AccessApprovalRequest
|
template: SmtpTemplates.AccessApprovalRequest
|
||||||
|
@ -8,6 +8,7 @@ export enum ApprovalStatus {
|
|||||||
|
|
||||||
export type TVerifyPermission = {
|
export type TVerifyPermission = {
|
||||||
permissions: unknown;
|
permissions: unknown;
|
||||||
|
checkPath?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetAccessRequestCountDTO = {
|
export type TGetAccessRequestCountDTO = {
|
||||||
@ -21,7 +22,15 @@ export type TReviewAccessRequestDTO = {
|
|||||||
|
|
||||||
export type TCreateAccessApprovalRequestDTO = {
|
export type TCreateAccessApprovalRequestDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
permissions: unknown;
|
environment: string;
|
||||||
|
// permissions: unknown;
|
||||||
|
requestedActions: {
|
||||||
|
read: boolean;
|
||||||
|
edit: boolean;
|
||||||
|
create: boolean;
|
||||||
|
delete: boolean;
|
||||||
|
};
|
||||||
|
secretPaths: string[];
|
||||||
isTemporary: boolean;
|
isTemporary: boolean;
|
||||||
temporaryRange?: string;
|
temporaryRange?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@ -232,7 +232,7 @@ export const permissionServiceFactory = ({
|
|||||||
objectify(
|
objectify(
|
||||||
userProjectPermission.metadata,
|
userProjectPermission.metadata,
|
||||||
(i) => i.key,
|
(i) => i.key,
|
||||||
(i) => i.value
|
(i) => i.value || ""
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const interpolateRules = templatedRules(
|
const interpolateRules = templatedRules(
|
||||||
@ -299,7 +299,7 @@ export const permissionServiceFactory = ({
|
|||||||
objectify(
|
objectify(
|
||||||
identityProjectPermission.metadata,
|
identityProjectPermission.metadata,
|
||||||
(i) => i.key,
|
(i) => i.key,
|
||||||
(i) => i.value
|
(i) => i.value || ""
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -135,7 +135,8 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
|
|||||||
parentMapper: (data) => ({
|
parentMapper: (data) => ({
|
||||||
environment: { id: data.envId, name: data.envName, slug: data.envSlug },
|
environment: { id: data.envId, name: data.envName, slug: data.envSlug },
|
||||||
projectId: data.projectId,
|
projectId: data.projectId,
|
||||||
...SecretApprovalPoliciesSchema.parse(data)
|
...SecretApprovalPoliciesSchema.parse(data),
|
||||||
|
secretPaths: data.secretPaths as string[]
|
||||||
}),
|
}),
|
||||||
childrenMapper: [
|
childrenMapper: [
|
||||||
{
|
{
|
||||||
|
@ -22,10 +22,22 @@ import {
|
|||||||
TUpdateSapDTO
|
TUpdateSapDTO
|
||||||
} from "./secret-approval-policy-types";
|
} from "./secret-approval-policy-types";
|
||||||
|
|
||||||
const getPolicyScore = (policy: { secretPath?: string | null }) =>
|
/*
|
||||||
// if glob pattern score is 1, if not exist score is 0 and if its not both then its exact path meaning score 2
|
* '1': The secret path is a glob pattern
|
||||||
// eslint-disable-next-line
|
* '0': The secret path is not defined (whole environment is scoped)
|
||||||
policy.secretPath ? (containsGlobPatterns(policy.secretPath) ? 1 : 2) : 0;
|
* '2': The secret path is an exact path
|
||||||
|
*/
|
||||||
|
const getPolicyScore = (policy: { secretPaths: string[] }) => {
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (!policy.secretPaths.length) return 0;
|
||||||
|
|
||||||
|
for (const secretPath of policy.secretPaths) {
|
||||||
|
score += containsGlobPatterns(secretPath) ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
};
|
||||||
|
|
||||||
type TSecretApprovalPolicyServiceFactoryDep = {
|
type TSecretApprovalPolicyServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
@ -55,7 +67,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
projectId,
|
projectId,
|
||||||
secretPath,
|
secretPaths,
|
||||||
environment,
|
environment,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
}: TCreateSapDTO) => {
|
}: TCreateSapDTO) => {
|
||||||
@ -105,7 +117,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
{
|
{
|
||||||
envId: env.id,
|
envId: env.id,
|
||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPaths: JSON.stringify(secretPaths),
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
},
|
},
|
||||||
@ -153,12 +165,12 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...secretApproval, environment: env, projectId };
|
return { ...secretApproval, environment: env, projectId, secretPaths };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSecretApprovalPolicy = async ({
|
const updateSecretApprovalPolicy = async ({
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPaths,
|
||||||
name,
|
name,
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@ -209,7 +221,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
secretApprovalPolicy.id,
|
secretApprovalPolicy.id,
|
||||||
{
|
{
|
||||||
approvals,
|
approvals,
|
||||||
secretPath,
|
secretPaths: secretPaths ? JSON.stringify(secretPaths) : undefined,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
},
|
},
|
||||||
@ -261,7 +273,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc;
|
return { ...doc, secretPaths: doc.secretPaths as string[] };
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
...updatedSap,
|
...updatedSap,
|
||||||
@ -302,7 +314,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await secretApprovalPolicyDAL.deleteById(secretPolicyId);
|
await secretApprovalPolicyDAL.deleteById(secretPolicyId);
|
||||||
return sapPolicy;
|
return { ...sapPolicy, secretPaths: sapPolicy.secretPaths as string[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretApprovalPolicyByProjectId = async ({
|
const getSecretApprovalPolicyByProjectId = async ({
|
||||||
@ -325,8 +337,9 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
return sapPolicies;
|
return sapPolicies;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSecretApprovalPolicy = async (projectId: string, environment: string, path: string) => {
|
const getSecretApprovalPolicy = async (projectId: string, environment: string, paths: string[] | string) => {
|
||||||
const secretPath = removeTrailingSlash(path);
|
const secretPaths = (Array.isArray(paths) ? paths : [paths]).map((p) => removeTrailingSlash(p).trim());
|
||||||
|
|
||||||
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
const env = await projectEnvDAL.findOne({ slug: environment, projectId });
|
||||||
if (!env) {
|
if (!env) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@ -336,14 +349,24 @@ export const secretApprovalPolicyServiceFactory = ({
|
|||||||
|
|
||||||
const policies = await secretApprovalPolicyDAL.find({ envId: env.id });
|
const policies = await secretApprovalPolicyDAL.find({ envId: env.id });
|
||||||
if (!policies.length) return;
|
if (!policies.length) return;
|
||||||
// this will filter policies either without scoped to secret path or the one that matches with secret path
|
|
||||||
const policiesFilteredByPath = policies.filter(
|
// A policy matches if either:
|
||||||
({ secretPath: policyPath }) => !policyPath || picomatch.isMatch(secretPath, policyPath, { strictSlashes: false })
|
// 1. It has no secretPaths (applies to all paths)
|
||||||
|
// 2. At least one of the provided secretPaths matches at least one of the policy paths
|
||||||
|
const matchingPolicies = policies.filter((policy) => {
|
||||||
|
if (!policy.secretPaths.length) return true; // Policy applies to all paths
|
||||||
|
|
||||||
|
// For each provided secret path, check if it matches any of the policy paths
|
||||||
|
return secretPaths.some((secretPath) =>
|
||||||
|
policy.secretPaths.some((policyPath) => picomatch.isMatch(secretPath, policyPath, { strictSlashes: false }))
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
|
// now sort by priority. exact secret path gets first match followed by glob followed by just env scoped
|
||||||
// if that is tie get by first createdAt
|
// if that is tie get by first createdAt
|
||||||
const policiesByPriority = policiesFilteredByPath.sort((a, b) => getPolicyScore(b) - getPolicyScore(a));
|
const policiesByPriority = matchingPolicies.sort((a, b) => getPolicyScore(b) - getPolicyScore(a));
|
||||||
const finalPolicy = policiesByPriority.shift();
|
const finalPolicy = policiesByPriority.shift();
|
||||||
|
|
||||||
return finalPolicy;
|
return finalPolicy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { ApproverType } from "../access-approval-policy/access-approval-policy-t
|
|||||||
|
|
||||||
export type TCreateSapDTO = {
|
export type TCreateSapDTO = {
|
||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath?: string | null;
|
secretPaths: string[];
|
||||||
environment: string;
|
environment: string;
|
||||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -15,7 +15,7 @@ export type TCreateSapDTO = {
|
|||||||
export type TUpdateSapDTO = {
|
export type TUpdateSapDTO = {
|
||||||
secretPolicyId: string;
|
secretPolicyId: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
secretPath?: string | null;
|
secretPaths?: string[];
|
||||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||||
name?: string;
|
name?: string;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
@ -108,7 +108,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
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("secretPaths").withSchema(TableName.SecretApprovalPolicy).as("policySecretPaths"),
|
||||||
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
|
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
|
||||||
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
|
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
|
||||||
@ -145,7 +145,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
id: el.policyId,
|
id: el.policyId,
|
||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPaths: el.policySecretPaths as string[],
|
||||||
enforcementLevel: el.policyEnforcementLevel,
|
enforcementLevel: el.policyEnforcementLevel,
|
||||||
envId: el.policyEnvId
|
envId: el.policyEnvId
|
||||||
}
|
}
|
||||||
@ -323,7 +323,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.raw(
|
db.raw(
|
||||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||||
),
|
),
|
||||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
db.ref("secretPaths").withSchema(TableName.SecretApprovalPolicy).as("policySecretPaths"),
|
||||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
@ -352,7 +352,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
id: el.policyId,
|
id: el.policyId,
|
||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPaths: el.policySecretPaths as string[],
|
||||||
enforcementLevel: el.policyEnforcementLevel
|
enforcementLevel: el.policyEnforcementLevel
|
||||||
},
|
},
|
||||||
committerUser: {
|
committerUser: {
|
||||||
@ -470,7 +470,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
db.raw(
|
db.raw(
|
||||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||||
),
|
),
|
||||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
db.ref("secretPaths").withSchema(TableName.SecretApprovalPolicy).as("policySecretPaths"),
|
||||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||||
@ -499,7 +499,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
|||||||
id: el.policyId,
|
id: el.policyId,
|
||||||
name: el.policyName,
|
name: el.policyName,
|
||||||
approvals: el.policyApprovals,
|
approvals: el.policyApprovals,
|
||||||
secretPath: el.policySecretPath,
|
secretPaths: el.policySecretPaths as string[],
|
||||||
enforcementLevel: el.policyEnforcementLevel
|
enforcementLevel: el.policyEnforcementLevel
|
||||||
},
|
},
|
||||||
committerUser: {
|
committerUser: {
|
||||||
|
@ -294,10 +294,10 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
|
const [secretPath] = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
|
||||||
secretApprovalRequest.folderId
|
secretApprovalRequest.folderId
|
||||||
]);
|
]);
|
||||||
return { ...secretApprovalRequest, secretPath: secretPath?.[0]?.path || "/", commits: secrets };
|
return { ...secretApprovalRequest, secretPath: secretPath?.path || "/", commits: secrets };
|
||||||
};
|
};
|
||||||
|
|
||||||
const reviewApproval = async ({
|
const reviewApproval = async ({
|
||||||
@ -831,7 +831,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
|
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
|
||||||
requesterEmail: requestedByUser.email,
|
requesterEmail: requestedByUser.email,
|
||||||
bypassReason,
|
bypassReason,
|
||||||
secretPath: policy.secretPath,
|
secretPath: folder.path,
|
||||||
environment: env.name,
|
environment: env.name,
|
||||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
||||||
},
|
},
|
||||||
|
@ -56,16 +56,15 @@ export const DefaultResponseErrorsSchema = {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sapPubSchema = SecretApprovalPoliciesSchema.merge(
|
export const sapPubSchema = SecretApprovalPoliciesSchema.extend({
|
||||||
z.object({
|
secretPaths: z.string().array(),
|
||||||
environment: z.object({
|
environment: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string()
|
slug: z.string()
|
||||||
}),
|
}),
|
||||||
projectId: z.string()
|
projectId: z.string()
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const sanitizedServiceTokenUserSchema = UsersSchema.pick({
|
export const sanitizedServiceTokenUserSchema = UsersSchema.pick({
|
||||||
authMethods: true,
|
authMethods: true,
|
||||||
|
@ -207,7 +207,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
.object({
|
.object({
|
||||||
key: z.string().trim().min(1),
|
key: z.string().trim().min(1),
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
value: z.string().trim().min(1)
|
value: z.string().trim().min(1).nullable().optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
@ -135,7 +135,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
.object({
|
.object({
|
||||||
key: z.string().trim().min(1),
|
key: z.string().trim().min(1),
|
||||||
id: z.string().trim().min(1),
|
id: z.string().trim().min(1),
|
||||||
value: z.string().trim().min(1)
|
value: z.string().trim().min(1).nullable().optional()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { packRules } from "@casl/ability/extra";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
@ -22,7 +21,7 @@ export const useCreateAccessApprovalPolicy = () => {
|
|||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
name,
|
name,
|
||||||
secretPath,
|
secretPaths,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
|
||||||
@ -30,7 +29,7 @@ export const useCreateAccessApprovalPolicy = () => {
|
|||||||
projectSlug,
|
projectSlug,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPaths,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
});
|
});
|
||||||
@ -46,11 +45,11 @@ export const useUpdateAccessApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
|
return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
|
||||||
mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => {
|
mutationFn: async ({ id, approvers, approvals, name, secretPaths, enforcementLevel }) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPaths,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
});
|
});
|
||||||
@ -83,8 +82,8 @@ export const useCreateAccessRequest = () => {
|
|||||||
const { data } = await apiRequest.post<TAccessApproval>(
|
const { data } = await apiRequest.post<TAccessApproval>(
|
||||||
"/api/v1/access-approvals/requests",
|
"/api/v1/access-approvals/requests",
|
||||||
{
|
{
|
||||||
...request,
|
...request
|
||||||
permissions: request.permissions ? packRules(request.permissions) : undefined
|
// permissions: request.permissions ? packRules(request.permissions) : undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
|
@ -73,7 +73,7 @@ const fetchApprovalRequests = async ({
|
|||||||
{ params: { projectSlug, envSlug, authorProjectMembershipId } }
|
{ params: { projectSlug, envSlug, authorProjectMembershipId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.requests.map((request) => ({
|
const result = data.requests.map((request) => ({
|
||||||
...request,
|
...request,
|
||||||
|
|
||||||
privilege: request.privilege
|
privilege: request.privilege
|
||||||
@ -86,6 +86,8 @@ const fetchApprovalRequests = async ({
|
|||||||
: null,
|
: null,
|
||||||
permissions: unpackRules(request.permissions as unknown as PackRule<TProjectPermission>[])
|
permissions: unpackRules(request.permissions as unknown as PackRule<TProjectPermission>[])
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAccessRequestsCount = async (projectSlug: string) => {
|
const fetchAccessRequestsCount = async (projectSlug: string) => {
|
||||||
|
@ -6,7 +6,7 @@ export type TAccessApprovalPolicy = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
approvals: number;
|
approvals: number;
|
||||||
secretPath: string;
|
secretPaths: string[];
|
||||||
envId: string;
|
envId: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
environment: WorkspaceEnv;
|
environment: WorkspaceEnv;
|
||||||
@ -18,15 +18,15 @@ export type TAccessApprovalPolicy = {
|
|||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ApproverType{
|
export enum ApproverType {
|
||||||
User = "user",
|
User = "user",
|
||||||
Group = "group"
|
Group = "group"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Approver ={
|
export type Approver = {
|
||||||
id: string;
|
id: string;
|
||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TAccessApprovalRequest = {
|
export type TAccessApprovalRequest = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -67,7 +67,7 @@ export type TAccessApprovalRequest = {
|
|||||||
name: string;
|
name: string;
|
||||||
approvals: number;
|
approvals: number;
|
||||||
approvers: string[];
|
approvers: string[];
|
||||||
secretPath?: string | null;
|
secretPaths?: string[] | null;
|
||||||
envId: string;
|
envId: string;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
};
|
};
|
||||||
@ -116,7 +116,18 @@ export type TProjectUserPrivilege = {
|
|||||||
|
|
||||||
export type TCreateAccessRequestDTO = {
|
export type TCreateAccessRequestDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
|
secretPaths: string[];
|
||||||
|
environment: string;
|
||||||
|
requestedActions: {
|
||||||
|
read: boolean;
|
||||||
|
edit: boolean;
|
||||||
|
create: boolean;
|
||||||
|
delete: boolean;
|
||||||
|
};
|
||||||
|
} & Omit<
|
||||||
|
TProjectUserPrivilege,
|
||||||
|
"id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId" | "permissions"
|
||||||
|
>;
|
||||||
|
|
||||||
export type TGetAccessApprovalRequestsDTO = {
|
export type TGetAccessApprovalRequestsDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
@ -132,7 +143,7 @@ export type TGetAccessPolicyApprovalCountDTO = {
|
|||||||
export type TGetSecretApprovalPolicyOfBoardDTO = {
|
export type TGetSecretApprovalPolicyOfBoardDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPaths: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateAccessPolicyDTO = {
|
export type TCreateAccessPolicyDTO = {
|
||||||
@ -141,7 +152,7 @@ export type TCreateAccessPolicyDTO = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
secretPath?: string;
|
secretPaths?: string[];
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -149,7 +160,7 @@ export type TUpdateAccessPolicyDTO = {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
secretPath?: string;
|
secretPaths?: string[];
|
||||||
environment?: string;
|
environment?: string;
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
|
@ -37,7 +37,7 @@ export type IdentityMembershipOrg = {
|
|||||||
id: string;
|
id: string;
|
||||||
identity: Identity;
|
identity: Identity;
|
||||||
organization: string;
|
organization: string;
|
||||||
metadata: { key: string; value: string; id: string }[];
|
metadata: { key: string; value?: string | null; id: string }[];
|
||||||
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
||||||
customRole?: TOrgRole;
|
customRole?: TOrgRole;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
@ -14,7 +14,7 @@ export const useCreateSecretApprovalPolicy = () => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPaths,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
}) => {
|
}) => {
|
||||||
@ -23,7 +23,7 @@ export const useCreateSecretApprovalPolicy = () => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPaths,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
});
|
});
|
||||||
@ -39,11 +39,11 @@ export const useUpdateSecretApprovalPolicy = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
||||||
mutationFn: async ({ id, approvers, approvals, secretPath, name, enforcementLevel }) => {
|
mutationFn: async ({ id, approvers, approvals, secretPaths, name, enforcementLevel }) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||||
approvals,
|
approvals,
|
||||||
approvers,
|
approvers,
|
||||||
secretPath,
|
secretPaths,
|
||||||
name,
|
name,
|
||||||
enforcementLevel
|
enforcementLevel
|
||||||
});
|
});
|
||||||
|
@ -7,22 +7,22 @@ export type TSecretApprovalPolicy = {
|
|||||||
name: string;
|
name: string;
|
||||||
envId: string;
|
envId: string;
|
||||||
environment: WorkspaceEnv;
|
environment: WorkspaceEnv;
|
||||||
secretPath?: string;
|
secretPaths: string[];
|
||||||
approvals: number;
|
approvals: number;
|
||||||
approvers: Approver[];
|
approvers: Approver[];
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ApproverType{
|
export enum ApproverType {
|
||||||
User = "user",
|
User = "user",
|
||||||
Group = "group"
|
Group = "group"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Approver ={
|
export type Approver = {
|
||||||
id: string;
|
id: string;
|
||||||
type: ApproverType;
|
type: ApproverType;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type TGetSecretApprovalPoliciesDTO = {
|
export type TGetSecretApprovalPoliciesDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -38,7 +38,7 @@ export type TCreateSecretPolicyDTO = {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath?: string | null;
|
secretPaths: string[];
|
||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
@ -48,7 +48,7 @@ export type TUpdateSecretPolicyDTO = {
|
|||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
secretPath?: string | null;
|
secretPaths?: string[];
|
||||||
approvals?: number;
|
approvals?: number;
|
||||||
enforcementLevel?: EnforcementLevel;
|
enforcementLevel?: EnforcementLevel;
|
||||||
// for invalidating list
|
// for invalidating list
|
||||||
|
@ -51,7 +51,7 @@ export type UserEnc = {
|
|||||||
|
|
||||||
export type OrgUser = {
|
export type OrgUser = {
|
||||||
id: string;
|
id: string;
|
||||||
metadata: { key: string; value: string; id: string }[];
|
metadata: { key: string; value?: string | null; id: string }[];
|
||||||
user: {
|
user: {
|
||||||
username: string;
|
username: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { ReservedFolders } from "@app/hooks/api/secretFolders/types";
|
import { ReservedFolders } from "@app/hooks/api/secretFolders/types";
|
||||||
|
|
||||||
export const formatReservedPaths = (secretPath: string) => {
|
export const formatReservedPaths = (paths: string | string[]) => {
|
||||||
const i = secretPath.indexOf(ReservedFolders.SecretReplication);
|
const secretPaths = Array.isArray(paths) ? paths : [paths];
|
||||||
|
|
||||||
|
const formattedSecretPaths = secretPaths.map((secretPath) => {
|
||||||
|
const i = secretPath?.indexOf(ReservedFolders.SecretReplication);
|
||||||
if (i !== -1) {
|
if (i !== -1) {
|
||||||
return `${secretPath.slice(0, i)} - (replication)`;
|
return `${secretPath.slice(0, i)} - (replication)`;
|
||||||
}
|
}
|
||||||
return secretPath;
|
return secretPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedSecretPaths.join(", ");
|
||||||
};
|
};
|
||||||
|
@ -51,7 +51,7 @@ import {
|
|||||||
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
|
||||||
const secretPermissionSchema = z.object({
|
const secretPermissionSchema = z.object({
|
||||||
secretPath: z.string().optional(),
|
secretPaths: z.string().optional(),
|
||||||
environmentSlug: z.string(),
|
environmentSlug: z.string(),
|
||||||
[ProjectPermissionActions.Edit]: z.boolean().optional(),
|
[ProjectPermissionActions.Edit]: z.boolean().optional(),
|
||||||
[ProjectPermissionActions.Read]: z.boolean().optional(),
|
[ProjectPermissionActions.Read]: z.boolean().optional(),
|
||||||
@ -99,7 +99,7 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
? {
|
? {
|
||||||
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
||||||
// secret path will be inside $glob operator
|
// secret path will be inside $glob operator
|
||||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob
|
secretPaths: privilege.permissions?.[0]?.conditions?.secretPath?.$glob
|
||||||
? removeTrailingSlash(privilege.permissions?.[0]?.conditions?.secretPath?.$glob)
|
? removeTrailingSlash(privilege.permissions?.[0]?.conditions?.secretPath?.$glob)
|
||||||
: "",
|
: "",
|
||||||
read: privilege.permissions?.some(({ action }) =>
|
read: privilege.permissions?.some(({ action }) =>
|
||||||
@ -132,7 +132,7 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
|
|
||||||
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
||||||
const selectedEnvironment = privilegeForm.watch("environmentSlug");
|
const selectedEnvironment = privilegeForm.watch("environmentSlug");
|
||||||
const secretPath = privilegeForm.watch("secretPath");
|
const secretPath = privilegeForm.watch("secretPaths");
|
||||||
|
|
||||||
const readAccess = privilegeForm.watch("read");
|
const readAccess = privilegeForm.watch("read");
|
||||||
const createAccess = privilegeForm.watch("create");
|
const createAccess = privilegeForm.watch("create");
|
||||||
@ -147,11 +147,11 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
(policy) => policy.environment.slug === selectedEnvironment
|
(policy) => policy.environment.slug === selectedEnvironment
|
||||||
);
|
);
|
||||||
|
|
||||||
privilegeForm.setValue("secretPath", "", {
|
privilegeForm.setValue("secretPaths", "", {
|
||||||
shouldValidate: true
|
shouldValidate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...environmentPolicies.map((policy) => policy.secretPath)];
|
return [...environmentPolicies.map((policy) => policy.secretPaths)];
|
||||||
}, [policies, selectedEnvironment]);
|
}, [policies, selectedEnvironment]);
|
||||||
|
|
||||||
const isTemporary = temporaryAccessField?.isTemporary;
|
const isTemporary = temporaryAccessField?.isTemporary;
|
||||||
@ -199,7 +199,7 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.secretPath) {
|
if (!data.secretPaths) {
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: "Please select a secret path",
|
text: "Please select a secret path",
|
||||||
@ -208,30 +208,21 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = [
|
|
||||||
{ action: ProjectPermissionActions.Read, allowed: data.read },
|
|
||||||
{ action: ProjectPermissionActions.Create, allowed: data.create },
|
|
||||||
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
|
||||||
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
|
||||||
];
|
|
||||||
const conditions: Record<string, any> = { environment: data.environmentSlug };
|
|
||||||
if (data.secretPath) {
|
|
||||||
conditions.secretPath = { $glob: data.secretPath };
|
|
||||||
}
|
|
||||||
await requestAccess.mutateAsync({
|
await requestAccess.mutateAsync({
|
||||||
...data,
|
...data,
|
||||||
...(data.temporaryAccess.isTemporary && {
|
...(data.temporaryAccess.isTemporary && {
|
||||||
temporaryRange: data.temporaryAccess.temporaryRange
|
temporaryRange: data.temporaryAccess.temporaryRange
|
||||||
}),
|
}),
|
||||||
|
secretPaths: JSON.parse(data.secretPaths) as string[],
|
||||||
|
environment: data.environmentSlug,
|
||||||
|
requestedActions: {
|
||||||
|
read: data.read || false,
|
||||||
|
create: data.create || false,
|
||||||
|
edit: data.edit || false,
|
||||||
|
delete: data.delete || false
|
||||||
|
},
|
||||||
projectSlug: currentWorkspace.slug,
|
projectSlug: currentWorkspace.slug,
|
||||||
isTemporary: data.temporaryAccess.isTemporary,
|
isTemporary: data.temporaryAccess.isTemporary
|
||||||
permissions: actions
|
|
||||||
.filter(({ allowed }) => allowed)
|
|
||||||
.map(({ action }) => ({
|
|
||||||
action,
|
|
||||||
subject: [ProjectPermissionSub.Secrets],
|
|
||||||
conditions
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@ -285,7 +276,7 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={privilegeForm.control}
|
control={privilegeForm.control}
|
||||||
name="secretPath"
|
name="secretPaths"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
if (policies) {
|
if (policies) {
|
||||||
return (
|
return (
|
||||||
@ -294,18 +285,28 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
content="The selected environment doesn't have any policies."
|
content="The selected environment doesn't have any policies."
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<FormControl label="Secret Path">
|
<FormControl label="Secret Paths">
|
||||||
<Select
|
<Select
|
||||||
{...field}
|
{...field}
|
||||||
isDisabled={isMemberEditDisabled || !selectablePaths.length}
|
isDisabled={isMemberEditDisabled || !selectablePaths.length}
|
||||||
className="w-48"
|
className="w-48"
|
||||||
onValueChange={(e) => field.onChange(e)}
|
onValueChange={(e) => field.onChange(e)}
|
||||||
>
|
>
|
||||||
{selectablePaths.map((path) => (
|
{selectablePaths.map((paths) => {
|
||||||
<SelectItem value={path} key={path}>
|
if (!paths || paths.length === 0) {
|
||||||
{path}
|
return (
|
||||||
|
<SelectItem value={JSON.stringify([])} key="empty">
|
||||||
|
Full Environment Access
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectItem value={JSON.stringify(paths)} key={paths.join("-")}>
|
||||||
|
{paths.join(", ")}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,7 +56,9 @@ const generateRequestText = (request: TAccessApprovalRequest, userId: string) =>
|
|||||||
<div>
|
<div>
|
||||||
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
Requested {isTemporary ? "temporary" : "permanent"} access to{" "}
|
||||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
||||||
{request.policy.secretPath}
|
{request.policy.secretPaths?.length
|
||||||
|
? request.policy.secretPaths.join(", ")
|
||||||
|
: "Full environment"}
|
||||||
</code>
|
</code>
|
||||||
in
|
in
|
||||||
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
<code className="mx-1 rounded-sm bg-primary-500/20 px-1.5 py-0.5 font-mono text-xs text-primary">
|
||||||
@ -120,19 +122,20 @@ export const AccessApprovalRequest = ({
|
|||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: requests } = useGetAccessApprovalRequests({
|
const { data: requests, isLoading: isRequestsLoading } = useGetAccessApprovalRequests({
|
||||||
projectSlug,
|
projectSlug,
|
||||||
authorProjectMembershipId: requestedByFilter,
|
authorProjectMembershipId: requestedByFilter,
|
||||||
envSlug: envFilter
|
envSlug: envFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredRequests = useMemo(() => {
|
const filteredRequests = useMemo(() => {
|
||||||
if (statusFilter === "open")
|
if (statusFilter === "open") {
|
||||||
return requests?.filter(
|
return requests?.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
!request.isApproved &&
|
!request.isApproved &&
|
||||||
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
!request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
if (statusFilter === "close")
|
if (statusFilter === "close")
|
||||||
return requests?.filter(
|
return requests?.filter(
|
||||||
(request) =>
|
(request) =>
|
||||||
@ -141,11 +144,10 @@ export const AccessApprovalRequest = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return requests;
|
return requests;
|
||||||
}, [requests, statusFilter, requestedByFilter, envFilter]);
|
return [];
|
||||||
|
}, [requests, statusFilter, requestedByFilter, envFilter, isRequestsLoading]);
|
||||||
|
|
||||||
const generateRequestDetails = (request: TAccessApprovalRequest) => {
|
const generateRequestDetails = (request: TAccessApprovalRequest) => {
|
||||||
console.log(request);
|
|
||||||
|
|
||||||
const isReviewedByUser = request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
|
const isReviewedByUser = request.reviewers.findIndex(({ member }) => member === user.id) !== -1;
|
||||||
const isRejectedByAnyone = request.reviewers.some(
|
const isRejectedByAnyone = request.reviewers.some(
|
||||||
({ status }) => status === ApprovalStatus.REJECTED
|
({ status }) => status === ApprovalStatus.REJECTED
|
||||||
|
@ -16,7 +16,7 @@ export const RequestAccessModal = ({
|
|||||||
<ModalContent
|
<ModalContent
|
||||||
className="max-w-4xl"
|
className="max-w-4xl"
|
||||||
title="Request Access"
|
title="Request Access"
|
||||||
subTitle="Your role has limited permissions, please contact your administrator to gain access"
|
subTitle="Request access to specific environments and paths across the project."
|
||||||
>
|
>
|
||||||
<SpecificPrivilegeSecretForm onClose={() => onOpenChange(false)} policies={policies} />
|
<SpecificPrivilegeSecretForm onClose={() => onOpenChange(false)} policies={policies} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@ -36,7 +36,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
const accessDetails = {
|
const accessDetails = {
|
||||||
env: request.environmentName,
|
env: request.environmentName,
|
||||||
// secret path will be inside $glob operator
|
// secret path will be inside $glob operator
|
||||||
secretPath: request.policy.secretPath,
|
secretPaths: request.policy.secretPaths,
|
||||||
read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)),
|
read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)),
|
||||||
edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)),
|
edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)),
|
||||||
create: request.permissions?.some(({ action }) =>
|
create: request.permissions?.some(({ action }) =>
|
||||||
@ -123,8 +123,16 @@ export const ReviewAccessRequestModal = ({
|
|||||||
|
|
||||||
<div className="mt-4 mb-2 border-l border-blue-500 bg-blue-500/20 px-3 py-2 text-mineshaft-200">
|
<div className="mt-4 mb-2 border-l border-blue-500 bg-blue-500/20 px-3 py-2 text-mineshaft-200">
|
||||||
<div className="mb-1 lowercase">
|
<div className="mb-1 lowercase">
|
||||||
<span className="font-bold capitalize">Requested path: </span>
|
<span className="font-bold capitalize">Requested environment: </span>
|
||||||
<Badge>{accessDetails.env + accessDetails.secretPath || ""}</Badge>
|
<Badge>{accessDetails.env}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="mb-1 lowercase">
|
||||||
|
<span className="font-bold capitalize">Requested paths: </span>
|
||||||
|
<Badge>
|
||||||
|
{accessDetails?.secretPaths?.length
|
||||||
|
? accessDetails.secretPaths.join(", ")
|
||||||
|
: "Entire environment"}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
@ -142,8 +150,7 @@ export const ReviewAccessRequestModal = ({
|
|||||||
<Button
|
<Button
|
||||||
isLoading={isLoading === "approved"}
|
isLoading={isLoading === "approved"}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!!isLoading ||
|
!!isLoading || (!request.isApprover && !byPassApproval && isSoftEnforcement)
|
||||||
(!request.isApprover && !byPassApproval && isSoftEnforcement)
|
|
||||||
}
|
}
|
||||||
onClick={() => handleReview("approved")}
|
onClick={() => handleReview("approved")}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
@ -169,9 +176,9 @@ export const ReviewAccessRequestModal = ({
|
|||||||
isChecked={byPassApproval}
|
isChecked={byPassApproval}
|
||||||
id="byPassApproval"
|
id="byPassApproval"
|
||||||
checkIndicatorBg="text-white"
|
checkIndicatorBg="text-white"
|
||||||
className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""}
|
className={byPassApproval ? "border-red bg-red hover:bg-red-600" : ""}
|
||||||
>
|
>
|
||||||
<span className="text-red text-sm">
|
<span className="text-sm text-red">
|
||||||
Approve without waiting for requirements to be met (bypass policy protection)
|
Approve without waiting for requirements to be met (bypass policy protection)
|
||||||
</span>
|
</span>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
@ -187,7 +187,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Environment</Th>
|
<Th>Environment</Th>
|
||||||
<Th>Secret Path</Th>
|
<Th>Secret Paths</Th>
|
||||||
<Th>Eligible Approvers</Th>
|
<Th>Eligible Approvers</Th>
|
||||||
<Th>Eligible Group Approvers</Th>
|
<Th>Eligible Group Approvers</Th>
|
||||||
<Th>Approval Required</Th>
|
<Th>Approval Required</Th>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faCheckCircle, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -14,6 +14,8 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
@ -22,7 +24,11 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { policyDetails } from "@app/helpers/policies";
|
import { policyDetails } from "@app/helpers/policies";
|
||||||
import { useCreateSecretApprovalPolicy, useListWorkspaceGroups, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
|
import {
|
||||||
|
useCreateSecretApprovalPolicy,
|
||||||
|
useListWorkspaceGroups,
|
||||||
|
useUpdateSecretApprovalPolicy
|
||||||
|
} from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
useCreateAccessApprovalPolicy,
|
useCreateAccessApprovalPolicy,
|
||||||
useUpdateAccessApprovalPolicy
|
useUpdateAccessApprovalPolicy
|
||||||
@ -44,9 +50,13 @@ const formSchema = z
|
|||||||
.object({
|
.object({
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
secretPath: z.string().optional(),
|
secretPaths: z.object({ path: z.string() }).array().default([]),
|
||||||
approvals: z.number().min(1),
|
approvals: z.number().min(1),
|
||||||
approvers: z.object({type: z.nativeEnum(ApproverType), id: z.string()}).array().min(1).default([]),
|
approvers: z
|
||||||
|
.object({ type: z.nativeEnum(ApproverType), id: z.string() })
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.default([]),
|
||||||
policyType: z.nativeEnum(PolicyType),
|
policyType: z.nativeEnum(PolicyType),
|
||||||
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
enforcementLevel: z.nativeEnum(EnforcementLevel)
|
||||||
})
|
})
|
||||||
@ -70,12 +80,16 @@ export const AccessPolicyForm = ({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
|
getValues,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
values: editValues
|
values: editValues
|
||||||
? {
|
? {
|
||||||
...editValues,
|
...editValues,
|
||||||
|
secretPaths: (editValues.secretPaths ? editValues.secretPaths : []).map((path) => ({
|
||||||
|
path
|
||||||
|
})),
|
||||||
environment: editValues.environment.slug,
|
environment: editValues.environment.slug,
|
||||||
approvers: editValues?.approvers || [],
|
approvers: editValues?.approvers || [],
|
||||||
approvals: editValues?.approvals
|
approvals: editValues?.approvals
|
||||||
@ -107,14 +121,23 @@ export const AccessPolicyForm = ({
|
|||||||
if (data.policyType === PolicyType.ChangePolicy) {
|
if (data.policyType === PolicyType.ChangePolicy) {
|
||||||
await createSecretApprovalPolicy({
|
await createSecretApprovalPolicy({
|
||||||
...data,
|
...data,
|
||||||
|
secretPaths: data.secretPaths.map((path) => path.path),
|
||||||
workspaceId: currentWorkspace?.id || ""
|
workspaceId: currentWorkspace?.id || ""
|
||||||
});
|
});
|
||||||
} else {
|
} else if (data.policyType === PolicyType.AccessPolicy) {
|
||||||
await createAccessApprovalPolicy({
|
await createAccessApprovalPolicy({
|
||||||
...data,
|
...data,
|
||||||
|
secretPaths: data.secretPaths.map((path) => path.path),
|
||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Invalid policy type"
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: "Successfully created policy"
|
text: "Successfully created policy"
|
||||||
@ -138,15 +161,24 @@ export const AccessPolicyForm = ({
|
|||||||
await updateSecretApprovalPolicy({
|
await updateSecretApprovalPolicy({
|
||||||
id: editValues?.id,
|
id: editValues?.id,
|
||||||
...data,
|
...data,
|
||||||
|
secretPaths: data.secretPaths.map((path) => path.path),
|
||||||
workspaceId: currentWorkspace?.id || ""
|
workspaceId: currentWorkspace?.id || ""
|
||||||
});
|
});
|
||||||
} else {
|
} else if (data.policyType === PolicyType.AccessPolicy) {
|
||||||
await updateAccessApprovalPolicy({
|
await updateAccessApprovalPolicy({
|
||||||
id: editValues?.id,
|
id: editValues?.id,
|
||||||
...data,
|
...data,
|
||||||
|
secretPaths: data.secretPaths.map((path) => path.path),
|
||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Invalid policy type"
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: "Successfully updated policy"
|
text: "Successfully updated policy"
|
||||||
@ -169,11 +201,10 @@ export const AccessPolicyForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatEnforcementLevel = (level: EnforcementLevel) => {
|
const secretPathsFields = useFieldArray({
|
||||||
if (level === EnforcementLevel.Hard) return "Hard";
|
control,
|
||||||
if (level === EnforcementLevel.Soft) return "Soft";
|
name: "secretPaths"
|
||||||
return level;
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
@ -252,19 +283,58 @@ export const AccessPolicyForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
<FormLabel
|
||||||
|
tooltipText="Select which secret paths in th environment that this policy should apply for."
|
||||||
|
label="Secret Paths"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 mt-1 flex flex-col space-y-2">
|
||||||
|
{secretPathsFields.fields.map(({ id: secretPathId }, i) => (
|
||||||
|
<div key={secretPathId} className="flex items-end space-x-2">
|
||||||
|
<div className="flex-grow">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="secretPath"
|
name={`secretPaths.${i}.path`}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Secret Path"
|
isError={Boolean(error?.message)}
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
>
|
>
|
||||||
<Input {...field} value={field.value || ""} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete key"
|
||||||
|
className="bottom-0.5 h-9"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
const secretPaths = getValues("secretPaths");
|
||||||
|
if (secretPaths && secretPaths?.length > 0) {
|
||||||
|
secretPathsFields.remove(i);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => secretPathsFields.append({ path: "/" })}
|
||||||
|
>
|
||||||
|
Add Path
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="approvals"
|
name="approvals"
|
||||||
@ -307,12 +377,8 @@ export const AccessPolicyForm = ({
|
|||||||
>
|
>
|
||||||
{Object.values(EnforcementLevel).map((level) => {
|
{Object.values(EnforcementLevel).map((level) => {
|
||||||
return (
|
return (
|
||||||
<SelectItem
|
<SelectItem value={level} key={`enforcement-level-${level}`}>
|
||||||
value={level}
|
<span className="capitalize">{level}</span>
|
||||||
key={`enforcement-level-${level}`}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{formatEnforcementLevel(level)}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -334,7 +400,11 @@ export const AccessPolicyForm = ({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
isReadOnly
|
||||||
value={value?.filter((e) => e.type=== ApproverType.User).length ? `${value.filter((e) => e.type=== ApproverType.User).length} selected` : "None"}
|
value={
|
||||||
|
value?.filter((e) => e.type === ApproverType.User).length
|
||||||
|
? `${value.filter((e) => e.type === ApproverType.User).length} selected`
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
className="text-left"
|
className="text-left"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -347,15 +417,22 @@ export const AccessPolicyForm = ({
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members.map(({ user }) => {
|
{members.map(({ user }) => {
|
||||||
const { id: userId } = user;
|
const { id: userId } = user;
|
||||||
const isChecked = value?.filter((el: {id: string, type: ApproverType}) => el.id === userId && el.type === ApproverType.User).length > 0;
|
const isChecked =
|
||||||
|
value?.filter(
|
||||||
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id === userId && el.type === ApproverType.User
|
||||||
|
).length > 0;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
onChange(
|
onChange(
|
||||||
isChecked
|
isChecked
|
||||||
? value?.filter((el: {id: string, type: ApproverType}) => el.id !== userId && el.type !== ApproverType.User)
|
? value?.filter(
|
||||||
: [...(value || []), {id:userId, type: ApproverType.User}]
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id !== userId && el.type !== ApproverType.User
|
||||||
|
)
|
||||||
|
: [...(value || []), { id: userId, type: ApproverType.User }]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${userId}`}
|
key={`create-policy-members-${userId}`}
|
||||||
@ -384,7 +461,13 @@ export const AccessPolicyForm = ({
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
isReadOnly
|
||||||
value={value?.filter((e) => e.type=== ApproverType.Group).length ? `${value?.filter((e) => e.type=== ApproverType.Group).length} selected` : "None"}
|
value={
|
||||||
|
value?.filter((e) => e.type === ApproverType.Group).length
|
||||||
|
? `${
|
||||||
|
value?.filter((e) => e.type === ApproverType.Group).length
|
||||||
|
} selected`
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
className="text-left"
|
className="text-left"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -395,9 +478,14 @@ export const AccessPolicyForm = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select groups that are allowed to approve requests
|
Select groups that are allowed to approve requests
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{groups && groups.map(({ group }) => {
|
{groups &&
|
||||||
|
groups.map(({ group }) => {
|
||||||
const { id } = group;
|
const { id } = group;
|
||||||
const isChecked = value?.filter((el: {id: string, type: ApproverType}) => el.id === id && el.type === ApproverType.Group).length > 0;
|
const isChecked =
|
||||||
|
value?.filter(
|
||||||
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id === id && el.type === ApproverType.Group
|
||||||
|
).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@ -405,8 +493,11 @@ export const AccessPolicyForm = ({
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
onChange(
|
onChange(
|
||||||
isChecked
|
isChecked
|
||||||
? value?.filter((el: {id: string, type: ApproverType}) => el.id !== id && el.type !== ApproverType.Group)
|
? value?.filter(
|
||||||
: [...(value || []), {id, type: ApproverType.Group}]
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id !== id && el.type !== ApproverType.Group
|
||||||
|
)
|
||||||
|
: [...(value || []), { id, type: ApproverType.Group }]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${id}`}
|
key={`create-policy-members-${id}`}
|
||||||
|
@ -29,13 +29,13 @@ interface IPolicy {
|
|||||||
name: string;
|
name: string;
|
||||||
environment: WorkspaceEnv;
|
environment: WorkspaceEnv;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
secretPath?: string;
|
secretPaths?: string[];
|
||||||
approvals: number;
|
approvals: number;
|
||||||
approvers?: Approver[];
|
approvers?: Approver[];
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
policyType: PolicyType;
|
policyType: PolicyType;
|
||||||
enforcementLevel: EnforcementLevel;
|
enforcementLevel: EnforcementLevel;
|
||||||
};
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
policy: IPolicy;
|
policy: IPolicy;
|
||||||
@ -56,10 +56,16 @@ export const ApprovalPolicyRow = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onDelete
|
onDelete
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [selectedApprovers, setSelectedApprovers] = useState<Approver[]>(policy.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
|
const [selectedApprovers, setSelectedApprovers] = useState<Approver[]>(
|
||||||
const [selectedGroupApprovers, setSelectedGroupApprovers] = useState<Approver[]>(policy.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
|
policy.approvers?.filter((approver) => approver.type === ApproverType.User) || []
|
||||||
const { mutate: updateAccessApprovalPolicy, isLoading: isAccessApprovalPolicyLoading } = useUpdateAccessApprovalPolicy();
|
);
|
||||||
const { mutate: updateSecretApprovalPolicy, isLoading: isSecretApprovalPolicyLoading } = useUpdateSecretApprovalPolicy();
|
const [selectedGroupApprovers, setSelectedGroupApprovers] = useState<Approver[]>(
|
||||||
|
policy.approvers?.filter((approver) => approver.type === ApproverType.Group) || []
|
||||||
|
);
|
||||||
|
const { mutate: updateAccessApprovalPolicy, isLoading: isAccessApprovalPolicyLoading } =
|
||||||
|
useUpdateAccessApprovalPolicy();
|
||||||
|
const { mutate: updateSecretApprovalPolicy, isLoading: isSecretApprovalPolicyLoading } =
|
||||||
|
useUpdateSecretApprovalPolicy();
|
||||||
const isLoading = isAccessApprovalPolicyLoading || isSecretApprovalPolicyLoading;
|
const isLoading = isAccessApprovalPolicyLoading || isSecretApprovalPolicyLoading;
|
||||||
|
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
@ -68,7 +74,7 @@ export const ApprovalPolicyRow = ({
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Td>{policy.name}</Td>
|
<Td>{policy.name}</Td>
|
||||||
<Td>{policy.environment.slug}</Td>
|
<Td>{policy.environment.slug}</Td>
|
||||||
<Td>{policy.secretPath || "*"}</Td>
|
<Td>{policy.secretPaths?.length ? policy?.secretPaths.join(", ") : "Full Environment"}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
@ -78,11 +84,15 @@ export const ApprovalPolicyRow = ({
|
|||||||
{
|
{
|
||||||
projectSlug,
|
projectSlug,
|
||||||
id: policy.id,
|
id: policy.id,
|
||||||
approvers: selectedApprovers.concat(selectedGroupApprovers),
|
approvers: selectedApprovers.concat(selectedGroupApprovers)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setSelectedApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
|
setSelectedApprovers(
|
||||||
|
policy?.approvers?.filter(
|
||||||
|
(approver) => approver.type === ApproverType.User
|
||||||
|
) || []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -91,17 +101,23 @@ export const ApprovalPolicyRow = ({
|
|||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id: policy.id,
|
id: policy.id,
|
||||||
approvers: selectedApprovers.concat(selectedGroupApprovers),
|
approvers: selectedApprovers.concat(selectedGroupApprovers)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setSelectedApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
|
setSelectedApprovers(
|
||||||
|
policy?.approvers?.filter(
|
||||||
|
(approver) => approver.type === ApproverType.User
|
||||||
|
) || []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setSelectedApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.User) || []);
|
setSelectedApprovers(
|
||||||
|
policy?.approvers?.filter((approver) => approver.type === ApproverType.User) || []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -127,13 +143,19 @@ export const ApprovalPolicyRow = ({
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{members?.map(({ user }) => {
|
{members?.map(({ user }) => {
|
||||||
const userId = user.id;
|
const userId = user.id;
|
||||||
const isChecked = selectedApprovers?.filter((el: { id: string, type: ApproverType }) => el.id === userId && el.type === ApproverType.User).length > 0;
|
const isChecked =
|
||||||
|
selectedApprovers?.filter(
|
||||||
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id === userId && el.type === ApproverType.User
|
||||||
|
).length > 0;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setSelectedApprovers((state) =>
|
setSelectedApprovers((state) =>
|
||||||
isChecked ? state.filter((el) => el.id !== userId || el.type !== ApproverType.User) : [...state, { id: userId, type: ApproverType.User }]
|
isChecked
|
||||||
|
? state.filter((el) => el.id !== userId || el.type !== ApproverType.User)
|
||||||
|
: [...state, { id: userId, type: ApproverType.User }]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
key={`create-policy-members-${userId}`}
|
key={`create-policy-members-${userId}`}
|
||||||
@ -156,37 +178,51 @@ export const ApprovalPolicyRow = ({
|
|||||||
{
|
{
|
||||||
projectSlug,
|
projectSlug,
|
||||||
id: policy.id,
|
id: policy.id,
|
||||||
approvers: selectedApprovers.concat(selectedGroupApprovers),
|
approvers: selectedApprovers.concat(selectedGroupApprovers)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setSelectedGroupApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
|
setSelectedGroupApprovers(
|
||||||
|
policy?.approvers?.filter(
|
||||||
|
(approver) => approver.type === ApproverType.Group
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
updateSecretApprovalPolicy(
|
updateSecretApprovalPolicy(
|
||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id: policy.id,
|
id: policy.id,
|
||||||
approvers: selectedApprovers.concat(selectedGroupApprovers),
|
approvers: selectedApprovers.concat(selectedGroupApprovers)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
setSelectedGroupApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
|
setSelectedGroupApprovers(
|
||||||
|
policy?.approvers?.filter(
|
||||||
|
(approver) => approver.type === ApproverType.Group
|
||||||
|
) || []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setSelectedGroupApprovers(policy?.approvers?.filter((approver) => approver.type === ApproverType.Group) || []);
|
setSelectedGroupApprovers(
|
||||||
|
policy?.approvers?.filter((approver) => approver.type === ApproverType.Group) || []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
isReadOnly
|
||||||
value={selectedGroupApprovers?.length ? `${selectedGroupApprovers.length} selected` : "None"}
|
value={
|
||||||
|
selectedGroupApprovers?.length
|
||||||
|
? `${selectedGroupApprovers.length} selected`
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
className="text-left"
|
className="text-left"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -197,16 +233,23 @@ export const ApprovalPolicyRow = ({
|
|||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Select groups that are allowed to approve requests
|
Select groups that are allowed to approve requests
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{groups && groups.map(({ group }) => {
|
{groups &&
|
||||||
|
groups.map(({ group }) => {
|
||||||
const { id } = group;
|
const { id } = group;
|
||||||
const isChecked = selectedGroupApprovers?.filter((el: { id: string, type: ApproverType }) => el.id === id && el.type === ApproverType.Group).length > 0;
|
const isChecked =
|
||||||
|
selectedGroupApprovers?.filter(
|
||||||
|
(el: { id: string; type: ApproverType }) =>
|
||||||
|
el.id === id && el.type === ApproverType.Group
|
||||||
|
).length > 0;
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(evt) => {
|
onClick={(evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
setSelectedGroupApprovers(
|
setSelectedGroupApprovers(
|
||||||
isChecked
|
isChecked
|
||||||
? selectedGroupApprovers?.filter((el) => el.id !== id || el.type !== ApproverType.Group)
|
? selectedGroupApprovers?.filter(
|
||||||
|
(el) => el.id !== id || el.type !== ApproverType.Group
|
||||||
|
)
|
||||||
: [...(selectedGroupApprovers || []), { id, type: ApproverType.Group }]
|
: [...(selectedGroupApprovers || []), { id, type: ApproverType.Group }]
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -229,12 +272,12 @@ export const ApprovalPolicyRow = ({
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg cursor-pointer">
|
<DropdownMenuTrigger asChild className="cursor-pointer rounded-lg">
|
||||||
<div className="flex justify-center items-center hover:text-primary-400 data-[state=open]:text-primary-400 hover:scale-125 data-[state=open]:scale-125 transition-transform duration-300 ease-in-out">
|
<div className="flex items-center justify-center transition-transform duration-300 ease-in-out hover:scale-125 hover:text-primary-400 data-[state=open]:scale-125 data-[state=open]:text-primary-400">
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="center" className="p-1 min-w-[100%]">
|
<DropdownMenuContent align="center" className="min-w-[100%] p-1">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.SecretApproval}
|
a={ProjectPermissionSub.SecretApproval}
|
||||||
|
Reference in New Issue
Block a user