mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 21:27:10 +00:00
feat: updated ui for new permission
This commit is contained in:
@@ -7,6 +7,30 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PermissionConditionOperators {
|
||||||
|
$IN = "$in",
|
||||||
|
$ALL = "$all",
|
||||||
|
$REGEX = "$regex",
|
||||||
|
$EQ = "$eq",
|
||||||
|
$NEQ = "$neq",
|
||||||
|
$GLOB = "$glob"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TPermissionConditionOperators = {
|
||||||
|
[PermissionConditionOperators.$IN]: string[];
|
||||||
|
[PermissionConditionOperators.$ALL]: string[];
|
||||||
|
[PermissionConditionOperators.$EQ]: string;
|
||||||
|
[PermissionConditionOperators.$NEQ]: string;
|
||||||
|
[PermissionConditionOperators.$REGEX]: string;
|
||||||
|
[PermissionConditionOperators.$GLOB]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPermissionCondition = Record<
|
||||||
|
string,
|
||||||
|
| string
|
||||||
|
| { $in: string[]; $all: string[]; $regex: string; $eq: string; $neq: string; $glob: string }
|
||||||
|
>;
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export type TPermission = {
|
|||||||
|
|
||||||
export type TProjectPermission = {
|
export type TProjectPermission = {
|
||||||
conditions?: Record<string, any>;
|
conditions?: Record<string, any>;
|
||||||
action: string;
|
action: string | string[];
|
||||||
subject: string | string[];
|
subject: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+409
-126
@@ -1,29 +1,39 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
import {
|
||||||
|
PermissionConditionOperators,
|
||||||
|
TPermissionCondition,
|
||||||
|
TPermissionConditionOperators
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
import { TProjectPermission } from "@app/hooks/api/roles/types";
|
import { TProjectPermission } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
const generalPermissionSchema = z
|
const GeneralPolicyActionSchema = z.object({
|
||||||
.object({
|
read: z.boolean().optional(),
|
||||||
read: z.boolean().optional(),
|
edit: z.boolean().optional(),
|
||||||
edit: z.boolean().optional(),
|
delete: z.boolean().optional(),
|
||||||
delete: z.boolean().optional(),
|
create: z.boolean().optional()
|
||||||
create: z.boolean().optional()
|
});
|
||||||
})
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
const multiEnvPermissionSchema = z
|
const SecretFolderPolicyActionSchema = z.object({
|
||||||
.object({
|
read: z.boolean().optional()
|
||||||
secretPath: z.string().trim().optional(),
|
});
|
||||||
read: z.boolean().optional(),
|
|
||||||
edit: z.boolean().optional(),
|
|
||||||
delete: z.boolean().optional(),
|
|
||||||
create: z.boolean().optional()
|
|
||||||
})
|
|
||||||
.optional();
|
|
||||||
|
|
||||||
const PERMISSION_ACTIONS = ["read", "create", "edit", "delete"] as const;
|
const SecretRollbackPolicyActionSchema = z.object({
|
||||||
|
read: z.boolean().optional(),
|
||||||
|
create: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const WorkspacePolicyActionSchema = z.object({
|
||||||
|
edit: z.boolean().optional(),
|
||||||
|
delete: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConditionSchema = z.object({
|
||||||
|
operator: z.string(),
|
||||||
|
lhs: z.string(),
|
||||||
|
rhs: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
@@ -35,139 +45,412 @@ export const formSchema = z.object({
|
|||||||
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
|
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
|
||||||
permissions: z
|
permissions: z
|
||||||
.object({
|
.object({
|
||||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
||||||
"secret-folders": generalPermissionSchema.optional(),
|
conditions: ConditionSchema.array().optional().default([])
|
||||||
member: generalPermissionSchema,
|
})
|
||||||
groups: generalPermissionSchema,
|
.array()
|
||||||
identity: generalPermissionSchema,
|
.default([]),
|
||||||
role: generalPermissionSchema,
|
[ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]),
|
||||||
integrations: generalPermissionSchema,
|
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
|
||||||
webhooks: generalPermissionSchema,
|
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"service-tokens": generalPermissionSchema,
|
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
|
||||||
settings: generalPermissionSchema,
|
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
|
||||||
environments: generalPermissionSchema,
|
[ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]),
|
||||||
tags: generalPermissionSchema,
|
[ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"ip-allowlist": generalPermissionSchema,
|
[ProjectPermissionSub.ServiceTokens]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"certificate-authorities": generalPermissionSchema,
|
[ProjectPermissionSub.Settings]: GeneralPolicyActionSchema.array().default([]),
|
||||||
certificates: generalPermissionSchema,
|
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"pki-alerts": generalPermissionSchema,
|
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"pki-collections": generalPermissionSchema,
|
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"certificate-templates": generalPermissionSchema,
|
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||||
// akhilmhdh: refactor all keys like below
|
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||||
workspace: z
|
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||||
.object({
|
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
edit: z.boolean().optional(),
|
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||||
delete: z.boolean().optional()
|
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||||
})
|
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]),
|
||||||
.optional(),
|
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
||||||
"secret-rollback": z
|
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
||||||
.object({
|
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([])
|
||||||
read: z.boolean().optional(),
|
|
||||||
create: z.boolean().optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
})
|
||||||
|
.partial()
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TFormSchema = z.infer<typeof formSchema>;
|
export type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const multiEnvApi2Form = (
|
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
const formConditions: z.infer<typeof ConditionSchema>[] = [];
|
||||||
permission: TProjectPermission
|
Object.entries(caslConditions).forEach(([type, condition]) => {
|
||||||
) => {
|
if (typeof condition === "string") {
|
||||||
const isCustomRule = Boolean(permission?.conditions?.environment);
|
formConditions.push({
|
||||||
// full access
|
operator: PermissionConditionOperators.$EQ,
|
||||||
if (isCustomRule && formVal && !formVal?.custom) {
|
lhs: type,
|
||||||
formVal.custom = { read: true, edit: true, delete: true, create: true };
|
rhs: condition
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
const secretEnv = permission?.conditions?.environment || "all";
|
Object.keys(condition).forEach((conditionOperator) => {
|
||||||
const secretPath = permission?.conditions?.secretPath?.$glob;
|
const rhs = condition[conditionOperator as PermissionConditionOperators];
|
||||||
// initialize
|
formConditions.push({
|
||||||
if (formVal && !formVal?.[secretEnv]) {
|
operator: conditionOperator,
|
||||||
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
lhs: type,
|
||||||
}
|
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
|
||||||
|
});
|
||||||
formVal[secretEnv][permission.action] = true;
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return formConditions;
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert role permission to form compatiable data structure
|
// convert role permission to form compatiable data structure
|
||||||
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||||
// any because if it set it as form type due to the discriminated union type of ts
|
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||||
// i would have to write a if loop with both conditions same
|
|
||||||
const formVal: Record<string, any> = {};
|
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
const { subject: caslSub, action } = permission;
|
const { subject: caslSub, action, conditions } = permission;
|
||||||
const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
|
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
|
||||||
if (!formVal?.[subject]) formVal[subject] = {};
|
|
||||||
|
|
||||||
if (subject === "secrets") {
|
if (
|
||||||
multiEnvApi2Form(formVal[subject], permission);
|
[
|
||||||
} else {
|
ProjectPermissionSub.Secrets,
|
||||||
// everything else follows same pattern
|
ProjectPermissionSub.Member,
|
||||||
// formVal[settings][read | write] = true
|
ProjectPermissionSub.Groups,
|
||||||
formVal[subject][action] = true;
|
ProjectPermissionSub.Identity,
|
||||||
|
ProjectPermissionSub.Role,
|
||||||
|
ProjectPermissionSub.Integrations,
|
||||||
|
ProjectPermissionSub.Webhooks,
|
||||||
|
ProjectPermissionSub.ServiceTokens,
|
||||||
|
ProjectPermissionSub.Settings,
|
||||||
|
ProjectPermissionSub.Environments,
|
||||||
|
ProjectPermissionSub.AuditLogs,
|
||||||
|
ProjectPermissionSub.IpAllowList,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities,
|
||||||
|
ProjectPermissionSub.Certificates,
|
||||||
|
ProjectPermissionSub.PkiAlerts,
|
||||||
|
ProjectPermissionSub.PkiCollections,
|
||||||
|
ProjectPermissionSub.CertificateTemplates,
|
||||||
|
ProjectPermissionSub.SecretApproval,
|
||||||
|
ProjectPermissionSub.Tags,
|
||||||
|
ProjectPermissionSub.SecretRotation,
|
||||||
|
ProjectPermissionSub.Kms
|
||||||
|
].includes(subject)
|
||||||
|
) {
|
||||||
|
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||||
|
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||||
|
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||||
|
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (subject === ProjectPermissionSub.Secrets) {
|
||||||
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
formVal[subject]!.push({
|
||||||
|
read: canRead,
|
||||||
|
create: canCreate,
|
||||||
|
edit: canEdit,
|
||||||
|
delete: canDelete,
|
||||||
|
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// deduplicate multiple rules for other policies
|
||||||
|
// because they don't have condition it doesn't make sense for multiple rules
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
|
if (canEdit) formVal[subject as ProjectPermissionSub.Member]![0].edit = true;
|
||||||
|
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
||||||
|
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
||||||
|
}
|
||||||
|
} else if (subject === ProjectPermissionSub.Workspace) {
|
||||||
|
const canEdit = action.includes(ProjectPermissionActions.Edit);
|
||||||
|
const canDelete = action.includes(ProjectPermissionActions.Delete);
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (canEdit) formVal[subject as ProjectPermissionSub.Workspace]![0].edit = true;
|
||||||
|
if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true;
|
||||||
|
} else if (subject === ProjectPermissionSub.SecretRollback) {
|
||||||
|
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||||
|
const canCreate = action.includes(ProjectPermissionActions.Create);
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
|
if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true;
|
||||||
|
} else if (subject === ProjectPermissionSub.SecretFolders) {
|
||||||
|
const canRead = action.includes(ProjectPermissionActions.Read);
|
||||||
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
|
||||||
|
// from above statement we are sure it won't be undefined
|
||||||
|
if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return formVal;
|
return formVal;
|
||||||
};
|
};
|
||||||
|
|
||||||
const multiEnvForm2Api = (
|
const convertFormOperatorToCaslCondition = (
|
||||||
permissions: TProjectPermission[],
|
conditions: { lhs: string; rhs: string; operator: string }[]
|
||||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
|
||||||
subject: "secrets"
|
|
||||||
) => {
|
) => {
|
||||||
if (!formVal) return;
|
const caslCondition: Record<string, Partial<TPermissionConditionOperators>> = {};
|
||||||
|
conditions.forEach((el) => {
|
||||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
if (!caslCondition[el.lhs]) caslCondition[el.lhs] = {};
|
||||||
// if any of them is set in all push it without any condition
|
if (
|
||||||
PERMISSION_ACTIONS.forEach((action) => {
|
el.operator === PermissionConditionOperators.$IN ||
|
||||||
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
el.operator === PermissionConditionOperators.$ALL
|
||||||
|
) {
|
||||||
|
caslCondition[el.lhs][el.operator] = el.rhs.split(",");
|
||||||
|
} else {
|
||||||
|
caslCondition[el.lhs][
|
||||||
|
el.operator as Exclude<
|
||||||
|
PermissionConditionOperators,
|
||||||
|
PermissionConditionOperators.$ALL | PermissionConditionOperators.$IN
|
||||||
|
>
|
||||||
|
] = el.rhs;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
return caslCondition;
|
||||||
if (!isFullAccess) {
|
|
||||||
Object.keys(formVal || {})
|
|
||||||
.filter((id) => id !== "all" && id !== "custom") // remove all and custom for iter
|
|
||||||
.forEach((slug) => {
|
|
||||||
const actions = Object.keys(formVal?.[slug] || {}) as [
|
|
||||||
"read",
|
|
||||||
"edit",
|
|
||||||
"create",
|
|
||||||
"delete",
|
|
||||||
"secretPath"
|
|
||||||
];
|
|
||||||
actions.forEach((action) => {
|
|
||||||
// if not full access for an action
|
|
||||||
if (!formVal?.all?.[action] && action !== "secretPath" && formVal?.[slug]?.[action]) {
|
|
||||||
const conditions: Record<string, unknown> = { environment: slug };
|
|
||||||
if (formVal[slug]?.secretPath)
|
|
||||||
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
|
|
||||||
|
|
||||||
permissions.push({ action, subject, conditions });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||||
const permissions: TProjectPermission[] = [];
|
const permissions: TProjectPermission[] = [];
|
||||||
// other than workspace everything else follows same
|
// other than workspace everything else follows same
|
||||||
// if in future there is a different follow the above on how workspace is done
|
// if in future there is a different follow the above on how workspace is done
|
||||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
Object.entries(formVal || {}).forEach(([subject, rules]) => {
|
||||||
if (rule === "secrets") {
|
rules.forEach((actions) => {
|
||||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
const caslActions = Object.keys(actions).filter(
|
||||||
} else if (actions) {
|
(el) => actions?.[el as keyof typeof actions]
|
||||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
);
|
||||||
if (isAllowed) {
|
const caslConditions =
|
||||||
permissions.push({ subject: rule, action });
|
"conditions" in actions
|
||||||
}
|
? convertFormOperatorToCaslCondition(actions.conditions)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
permissions.push({
|
||||||
|
action: caslActions,
|
||||||
|
subject: [subject],
|
||||||
|
conditions: caslConditions
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
return permissions;
|
return permissions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TProjectPermissionObject = {
|
||||||
|
[K in ProjectPermissionSub]: {
|
||||||
|
title: string;
|
||||||
|
actions: {
|
||||||
|
label: string;
|
||||||
|
value: keyof Omit<
|
||||||
|
NonNullable<NonNullable<TFormSchema["permissions"]>[K]>[number],
|
||||||
|
"conditions"
|
||||||
|
>;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
||||||
|
[ProjectPermissionSub.Secrets]: {
|
||||||
|
title: "Secrets",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretFolders]: {
|
||||||
|
title: "Secret Folders",
|
||||||
|
actions: [{ label: "Read", value: "read" }]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Kms]: {
|
||||||
|
title: "KMS",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Integrations]: {
|
||||||
|
title: "Integrations",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Workspace]: {
|
||||||
|
title: "Project",
|
||||||
|
actions: [
|
||||||
|
{ label: "Update project details", value: "edit" },
|
||||||
|
{ label: "Delete project", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Role]: {
|
||||||
|
title: "Roles",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Member]: {
|
||||||
|
title: "User Management",
|
||||||
|
actions: [
|
||||||
|
{ label: "View all members", value: "read" },
|
||||||
|
{ label: "Invite members", value: "create" },
|
||||||
|
{ label: "Edit members", value: "edit" },
|
||||||
|
{ label: "Remove members", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Groups]: {
|
||||||
|
title: "Group Management",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Identity]: {
|
||||||
|
title: "Machine Identity Management",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Webhooks]: {
|
||||||
|
title: "Webhooks",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.ServiceTokens]: {
|
||||||
|
title: "Service Tokens",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Settings]: {
|
||||||
|
title: "Settings",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Modify", value: "edit" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Environments]: {
|
||||||
|
title: "Environments",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Tags]: {
|
||||||
|
title: "Tags",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.AuditLogs]: {
|
||||||
|
title: "Audit Logs",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.IpAllowList]: {
|
||||||
|
title: "IP Allowlist",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.CertificateAuthorities]: {
|
||||||
|
title: "Certificate Authorities",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.Certificates]: {
|
||||||
|
title: "Certificates",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.CertificateTemplates]: {
|
||||||
|
title: "Certificate Templates",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.PkiCollections]: {
|
||||||
|
title: "PKI Collections",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.PkiAlerts]: {
|
||||||
|
title: "PKI Alerts",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretApproval]: {
|
||||||
|
title: "Secret Protect policy",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretRotation]: {
|
||||||
|
title: "Secret Rotation",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: "read" },
|
||||||
|
{ label: "Create", value: "create" },
|
||||||
|
{ label: "Modify", value: "edit" },
|
||||||
|
{ label: "Remove", value: "delete" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ProjectPermissionSub.SecretRollback]: {
|
||||||
|
title: "Secret Rollback",
|
||||||
|
actions: [
|
||||||
|
{ label: "Perform rollback", value: "create" },
|
||||||
|
{ label: "View", value: "read" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
-219
@@ -1,219 +0,0 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
const GENERAL_PERMISSIONS = [
|
|
||||||
{ action: "read", label: "View" },
|
|
||||||
{ action: "create", label: "Create" },
|
|
||||||
{ action: "edit", label: "Modify" },
|
|
||||||
{ action: "delete", label: "Remove" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const WORKSPACE_PERMISSIONS = [
|
|
||||||
{ action: "edit", label: "Update project details" },
|
|
||||||
{ action: "delete", label: "Delete projects" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const MEMBERS_PERMISSIONS = [
|
|
||||||
{ action: "read", label: "View all members" },
|
|
||||||
{ action: "create", label: "Invite members" },
|
|
||||||
{ action: "edit", label: "Edit members" },
|
|
||||||
{ action: "delete", label: "Remove members" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const SECRET_ROLLBACK_PERMISSIONS = [
|
|
||||||
{ action: "create", label: "Perform Rollback" },
|
|
||||||
{ action: "read", label: "View" }
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const getPermissionList = (option: Props["formName"]) => {
|
|
||||||
switch (option) {
|
|
||||||
case "workspace":
|
|
||||||
return WORKSPACE_PERMISSIONS;
|
|
||||||
case "member":
|
|
||||||
return MEMBERS_PERMISSIONS;
|
|
||||||
case "secret-rollback":
|
|
||||||
return SECRET_ROLLBACK_PERMISSIONS;
|
|
||||||
default:
|
|
||||||
return GENERAL_PERMISSIONS;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type PermissionName =
|
|
||||||
| `permissions.workspace.${"edit" | "delete"}`
|
|
||||||
| `permissions.secret-rollback.${"create" | "read"}`
|
|
||||||
| `permissions.${Exclude<
|
|
||||||
keyof NonNullable<TFormSchema["permissions"]>,
|
|
||||||
"workspace" | "secret-rollback" | "secrets"
|
|
||||||
>}.${"read" | "create" | "edit" | "delete"}`;
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isEditable: boolean;
|
|
||||||
title: string;
|
|
||||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "secrets">;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
NoAccess = "no-access",
|
|
||||||
ReadOnly = "read-only",
|
|
||||||
FullAccess = "full-acess",
|
|
||||||
Custom = "custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RolePermissionRow = ({ isEditable, title, formName, control, setValue }: Props) => {
|
|
||||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
|
||||||
const [isCustom, setIsCustom] = useToggle();
|
|
||||||
|
|
||||||
const rule = useWatch({
|
|
||||||
control,
|
|
||||||
name: `permissions.${formName}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedPermissionCategory = useMemo(() => {
|
|
||||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
|
||||||
|
|
||||||
switch (formName) {
|
|
||||||
default: {
|
|
||||||
const totalActions = GENERAL_PERMISSIONS.length;
|
|
||||||
const score = actions
|
|
||||||
.map((key) => (rule?.[key] ? 1 : 0))
|
|
||||||
.reduce((a, b) => a + b, 0 as number);
|
|
||||||
if (isCustom) return Permission.Custom;
|
|
||||||
if (score === 0) return Permission.NoAccess;
|
|
||||||
if (score === totalActions) return Permission.FullAccess;
|
|
||||||
if (rule && "read" in rule) {
|
|
||||||
if (score === 1 && rule?.read) return Permission.ReadOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Permission.Custom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [rule, isCustom]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
|
||||||
else setIsCustom.off();
|
|
||||||
}, [selectedPermissionCategory]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
|
||||||
if (isRowCustom) {
|
|
||||||
setIsRowExpanded.on();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (val === Permission.Custom) {
|
|
||||||
setIsRowExpanded.on();
|
|
||||||
setIsCustom.on();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsCustom.off();
|
|
||||||
|
|
||||||
switch (val) {
|
|
||||||
case Permission.NoAccess:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: false, edit: false, create: false, delete: false },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case Permission.FullAccess:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: true, edit: true, create: true, delete: true },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case Permission.ReadOnly:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: true, edit: false, create: false, delete: false },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ read: false, edit: false, create: false, delete: false },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tr
|
|
||||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
|
||||||
onClick={() => setIsRowExpanded.toggle()}
|
|
||||||
>
|
|
||||||
<Td>
|
|
||||||
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
|
||||||
</Td>
|
|
||||||
<Td>{title}</Td>
|
|
||||||
<Td>
|
|
||||||
<Select
|
|
||||||
value={selectedPermissionCategory}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
{isRowExpanded && (
|
|
||||||
<Tr>
|
|
||||||
<Td
|
|
||||||
colSpan={3}
|
|
||||||
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
{getPermissionList(formName).map(({ action, label }) => {
|
|
||||||
const permissionName = `permissions.${formName}.${action}` as PermissionName;
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name={permissionName}
|
|
||||||
key={permissionName}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={(e) => {
|
|
||||||
if (!isEditable) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to update default role"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
field.onChange(e);
|
|
||||||
}}
|
|
||||||
id={permissionName}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Checkbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
import { Control, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { Select, SelectItem, Td, Tr } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
|
||||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isEditable: boolean;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
SameAsSecrets = "same-as-secrets",
|
|
||||||
ReadOnly = "read-only"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RowPermissionSecretFoldersRow = ({ isEditable, setValue, control }: Props) => {
|
|
||||||
const formName = ProjectPermissionSub.SecretFolders;
|
|
||||||
const rule = useWatch({
|
|
||||||
control,
|
|
||||||
name: `permissions.${formName}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedPermissionCategory =
|
|
||||||
rule !== undefined ? Permission.ReadOnly : Permission.SameAsSecrets;
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (!val) return;
|
|
||||||
switch (val) {
|
|
||||||
case Permission.SameAsSecrets: {
|
|
||||||
setValue(`permissions.${formName}`, undefined, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Read-only
|
|
||||||
default:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{
|
|
||||||
read: true,
|
|
||||||
edit: false,
|
|
||||||
create: false,
|
|
||||||
delete: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
shouldDirty: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr>
|
|
||||||
<Td />
|
|
||||||
<Td>Secret Folders</Td>
|
|
||||||
<Td>
|
|
||||||
<Select
|
|
||||||
value={selectedPermissionCategory}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.SameAsSecrets}>Same as Secrets</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-248
@@ -1,248 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form";
|
|
||||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
FormControl,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Table,
|
|
||||||
TableContainer,
|
|
||||||
TBody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
THead,
|
|
||||||
Tr
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useWorkspace } from "@app/context";
|
|
||||||
import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
formName: "secrets";
|
|
||||||
isEditable: boolean;
|
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
|
||||||
getValue: UseFormGetValues<TFormSchema>;
|
|
||||||
control: Control<TFormSchema>;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Permission {
|
|
||||||
NoAccess = "no-access",
|
|
||||||
ReadOnly = "read-only",
|
|
||||||
FullAccess = "full-acess",
|
|
||||||
Custom = "custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RowPermissionSecretsRow = ({
|
|
||||||
title,
|
|
||||||
formName,
|
|
||||||
isEditable,
|
|
||||||
setValue,
|
|
||||||
getValue,
|
|
||||||
control
|
|
||||||
}: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
|
||||||
|
|
||||||
const customRule = useWatch({
|
|
||||||
control,
|
|
||||||
name: `permissions.${formName}.custom`
|
|
||||||
});
|
|
||||||
const isCustom = Boolean(customRule);
|
|
||||||
|
|
||||||
const allRule = useWatch({ control, name: `permissions.${formName}.all` });
|
|
||||||
|
|
||||||
const selectedPermissionCategory = useMemo(() => {
|
|
||||||
const { read, delete: del, edit, create } = allRule || {};
|
|
||||||
if (read && del && edit && create) return Permission.FullAccess;
|
|
||||||
if (read) return Permission.ReadOnly;
|
|
||||||
return Permission.NoAccess;
|
|
||||||
}, [allRule]);
|
|
||||||
|
|
||||||
const handlePermissionChange = (val: Permission) => {
|
|
||||||
if (!val) return;
|
|
||||||
switch (val) {
|
|
||||||
case Permission.NoAccess: {
|
|
||||||
const permissions = getValue("permissions");
|
|
||||||
if (permissions) delete permissions[formName];
|
|
||||||
setValue("permissions", permissions, { shouldDirty: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Permission.FullAccess:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ all: { read: true, edit: true, create: true, delete: true } },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case Permission.ReadOnly:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ all: { read: true, edit: false, create: false, delete: false } },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
setValue(
|
|
||||||
`permissions.${formName}`,
|
|
||||||
{ custom: { read: false, edit: false, create: false, delete: false } },
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tr>
|
|
||||||
<Td>{isCustom && <FontAwesomeIcon icon={faChevronDown} />}</Td>
|
|
||||||
<Td>{title}</Td>
|
|
||||||
<Td>
|
|
||||||
<Select
|
|
||||||
value={isCustom ? Permission.Custom : selectedPermissionCategory}
|
|
||||||
className="w-40 bg-mineshaft-600"
|
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
|
||||||
onValueChange={handlePermissionChange}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
>
|
|
||||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
|
||||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
|
||||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
{isCustom && (
|
|
||||||
<Tr>
|
|
||||||
<Td
|
|
||||||
colSpan={3}
|
|
||||||
className={`bg-bunker-600 px-0 py-0 ${isCustom && " border-mineshaft-500 p-8"}`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<TableContainer className="border-mineshaft-500">
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th />
|
|
||||||
<Th className="min-w-[8rem]">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
Secret Path
|
|
||||||
<span className="text-xs normal-case">
|
|
||||||
<GlobPatternExamples />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Th>
|
|
||||||
<Th className="text-center">View</Th>
|
|
||||||
<Th className="text-center">Create</Th>
|
|
||||||
<Th className="text-center">Modify</Th>
|
|
||||||
<Th className="text-center">Delete</Th>
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{isCustom &&
|
|
||||||
environments.map(({ name, slug }) => (
|
|
||||||
<Tr key={`custom-role-project-secret-${slug}`}>
|
|
||||||
<Td>{name}</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.secretPath`}
|
|
||||||
control={control}
|
|
||||||
defaultValue="/**"
|
|
||||||
render={({ field }) => (
|
|
||||||
/* eslint-disable-next-line no-template-curly-in-string */
|
|
||||||
<FormControl helperText="Supports glob path pattern string">
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
className="w-full overflow-ellipsis"
|
|
||||||
placeholder="Glob patterns are supported"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.read`}
|
|
||||||
control={control}
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
id={`permissions.${formName}.${slug}.read`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.create`}
|
|
||||||
control={control}
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
id={`permissions.${formName}.${slug}.modify`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
name={`permissions.${formName}.${slug}.edit`}
|
|
||||||
control={control}
|
|
||||||
defaultValue={false}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
id={`permissions.${formName}.${slug}.modify`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Controller
|
|
||||||
defaultValue={false}
|
|
||||||
name={`permissions.${formName}.${slug}.delete`}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
id={`permissions.${formName}.${slug}.delete`}
|
|
||||||
isDisabled={!isEditable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+76
-158
@@ -1,127 +1,52 @@
|
|||||||
import { useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
|
import { Button, Modal, ModalContent, ModalTrigger } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions";
|
||||||
|
import { NewPermissionRule } from "./components/NewPermissionRule";
|
||||||
|
import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
|
||||||
import {
|
import {
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
formSchema,
|
formSchema,
|
||||||
|
PROJECT_PERMISSION_OBJECT,
|
||||||
rolePermission2Form,
|
rolePermission2Form,
|
||||||
TFormSchema
|
TFormSchema
|
||||||
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
} from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
import { RolePermissionRow } from "./RolePermissionRow";
|
|
||||||
import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow";
|
|
||||||
import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow";
|
|
||||||
|
|
||||||
const SINGLE_PERMISSION_LIST = [
|
|
||||||
{
|
|
||||||
title: "Project",
|
|
||||||
formName: "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Integrations",
|
|
||||||
formName: "integrations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Secret Protect policy",
|
|
||||||
formName: ProjectPermissionSub.SecretApproval
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Roles",
|
|
||||||
formName: "role"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "User Management",
|
|
||||||
formName: "member"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Group Management",
|
|
||||||
formName: "groups"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Machine Identity Management",
|
|
||||||
formName: "identity"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Webhooks",
|
|
||||||
formName: "webhooks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Service Tokens",
|
|
||||||
formName: "service-tokens"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
formName: "settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Environments",
|
|
||||||
formName: "environments"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Tags",
|
|
||||||
formName: "tags"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "IP Allowlist",
|
|
||||||
formName: "ip-allowlist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Certificate Authorities",
|
|
||||||
formName: "certificate-authorities"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Certificates",
|
|
||||||
formName: "certificates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Certificate Templates",
|
|
||||||
formName: "certificate-templates"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "PKI Collections",
|
|
||||||
formName: "pki-collections"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "PKI Alerts",
|
|
||||||
formName: "pki-alerts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Secret Rollback",
|
|
||||||
formName: "secret-rollback"
|
|
||||||
}
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
roleSlug: string;
|
roleSlug: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
const { data: role } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string);
|
const { data: role } = useGetProjectRoleBySlug(currentWorkspace?.slug ?? "", roleSlug as string);
|
||||||
|
|
||||||
|
const form = useForm<TFormSchema>({
|
||||||
|
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
const {
|
const {
|
||||||
setValue,
|
|
||||||
getValues,
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isDirty, isSubmitting },
|
formState: { isDirty, isSubmitting },
|
||||||
reset
|
reset
|
||||||
} = useForm<TFormSchema>({
|
} = form;
|
||||||
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
|
||||||
resolver: zodResolver(formSchema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||||
|
|
||||||
const onSubmit = async (el: TFormSchema) => {
|
const onSubmit = async (el: TFormSchema) => {
|
||||||
try {
|
try {
|
||||||
if (!projectSlug || !role?.id) return;
|
if (!projectSlug || !role?.id) return;
|
||||||
|
|
||||||
await updateRole({
|
await updateRole({
|
||||||
id: role?.id as string,
|
id: role?.id as string,
|
||||||
projectSlug,
|
projectSlug,
|
||||||
@@ -143,70 +68,63 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
|||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
<FormProvider {...form}>
|
||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Permissions</h3>
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
{isCustomRole && (
|
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center space-x-4">
|
||||||
<Button
|
{isCustomRole && (
|
||||||
colorSchema="primary"
|
<>
|
||||||
type="submit"
|
<Button
|
||||||
isDisabled={isSubmitting || !isDirty}
|
colorSchema="primary"
|
||||||
isLoading={isSubmitting}
|
type="submit"
|
||||||
>
|
isDisabled={isSubmitting || !isDirty}
|
||||||
Save
|
isLoading={isSubmitting}
|
||||||
</Button>
|
>
|
||||||
<Button
|
Save
|
||||||
className="ml-4 text-mineshaft-300"
|
</Button>
|
||||||
variant="link"
|
<Button
|
||||||
isDisabled={isSubmitting || !isDirty}
|
className="ml-4 text-mineshaft-300"
|
||||||
isLoading={isSubmitting}
|
variant="link"
|
||||||
onClick={() => reset()}
|
isDisabled={isSubmitting || !isDirty}
|
||||||
>
|
isLoading={isSubmitting}
|
||||||
Cancel
|
onClick={() => reset()}
|
||||||
</Button>
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
<AnimatePresence>
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<TableContainer>
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
<Table>
|
<GeneralPermissionOptions
|
||||||
<THead>
|
subject={subject}
|
||||||
<Tr>
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
<Th className="w-5" />
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
<Th>Resource</Th>
|
key={`project-permission-${subject}`}
|
||||||
<Th>Permission</Th>
|
>
|
||||||
</Tr>
|
{subject === ProjectPermissionSub.Secrets ? (
|
||||||
</THead>
|
<SecretPermissionConditions />
|
||||||
<TBody>
|
) : undefined}
|
||||||
<RowPermissionSecretsRow
|
</GeneralPermissionOptions>
|
||||||
title="Secrets"
|
))}
|
||||||
formName={ProjectPermissionSub.Secrets}
|
</div>
|
||||||
isEditable={isCustomRole}
|
</AnimatePresence>
|
||||||
setValue={setValue}
|
<Modal
|
||||||
getValue={getValues}
|
isOpen={popUp.createPolicy.isOpen}
|
||||||
control={control}
|
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
||||||
/>
|
>
|
||||||
<RowPermissionSecretFoldersRow
|
<ModalTrigger asChild disabled={isDisabled}>
|
||||||
isEditable={isCustomRole}
|
<Button isDisabled={isDisabled} leftIcon={<FontAwesomeIcon icon={faPlus} />}>
|
||||||
setValue={setValue}
|
Add Policy
|
||||||
control={control}
|
</Button>
|
||||||
/>
|
</ModalTrigger>
|
||||||
{SINGLE_PERMISSION_LIST.map((permission) => {
|
<ModalContent title="New Policy" subTitle="Policies grant additional permissions.">
|
||||||
return (
|
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
||||||
<RolePermissionRow
|
</ModalContent>
|
||||||
title={permission.title}
|
</Modal>
|
||||||
formName={permission.formName}
|
</FormProvider>
|
||||||
control={control}
|
|
||||||
setValue={setValue}
|
|
||||||
key={`project-role-${roleSlug}-permission-${permission.formName}`}
|
|
||||||
isEditable={isCustomRole}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
import { cloneElement } from "react";
|
||||||
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { Button, Checkbox, Tag } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props<T extends ProjectPermissionSub> = {
|
||||||
|
title: string;
|
||||||
|
subject: T;
|
||||||
|
actions: TProjectPermissionObject[T]["actions"];
|
||||||
|
children?: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GeneralPermissionOptions = <T extends keyof NonNullable<TFormSchema["permissions"]>>({
|
||||||
|
subject,
|
||||||
|
actions,
|
||||||
|
children,
|
||||||
|
title
|
||||||
|
}: Props<T>) => {
|
||||||
|
const { control } = useFormContext<TFormSchema>();
|
||||||
|
const items = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `permissions.${subject}`
|
||||||
|
});
|
||||||
|
const [isOpen, setIsOpen] = useToggle();
|
||||||
|
|
||||||
|
if (!items.fields.length) return <div />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center space-x-8 px-5 py-4 text-sm text-gray-300"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => setIsOpen.toggle()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
setIsOpen.toggle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronRight} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">{title}</div>
|
||||||
|
{items.fields.length > 1 && (
|
||||||
|
<div>
|
||||||
|
<Tag size="xs" className="px-2">
|
||||||
|
{items.fields.length} rules
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isOpen && (
|
||||||
|
<motion.div
|
||||||
|
key={`select-${subject}-type`}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
initial={{ opacity: 0, translateY: -10 }}
|
||||||
|
animate={{ opacity: 1, translateY: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 10 }}
|
||||||
|
className="flex flex-col space-y-4 bg-bunker-800 p-6"
|
||||||
|
>
|
||||||
|
{items.fields.map((el, rootIndex) => (
|
||||||
|
<div key={el.id} className="bg-mineshaft-800 p-5 first:rounded-t-md last:rounded-b-md">
|
||||||
|
<div className="flex text-gray-300">
|
||||||
|
<div className="w-1/4">Actions</div>
|
||||||
|
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||||
|
{actions.map(({ label, value }) => {
|
||||||
|
if (typeof value !== "string") return undefined;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
key={`${el.id}-${label}`}
|
||||||
|
name={`permissions.${subject}.${rootIndex}.${value}` as any}
|
||||||
|
control={control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={Boolean(field.value)}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`permissions.${subject}.${rootIndex}.${String(value)}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children &&
|
||||||
|
cloneElement(children, {
|
||||||
|
position: rootIndex
|
||||||
|
})}
|
||||||
|
<div className="mt-2 flex justify-end space-x-4">
|
||||||
|
{subject === ProjectPermissionSub.Secrets && (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
variant="star"
|
||||||
|
size="xs"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={() => items.insert(rootIndex, [{} as any])}
|
||||||
|
>
|
||||||
|
Add rule
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
className="mt-2 hover:border-red"
|
||||||
|
onClick={() => items.remove(rootIndex)}
|
||||||
|
>
|
||||||
|
Remove Rule
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
ModalClose,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formSchema,
|
||||||
|
PROJECT_PERMISSION_OBJECT,
|
||||||
|
TFormSchema
|
||||||
|
} from "../ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewPermissionRule = ({ onClose }: Props) => {
|
||||||
|
const rootForm = useFormContext<TFormSchema>();
|
||||||
|
|
||||||
|
const form = useForm<{
|
||||||
|
type: ProjectPermissionSub;
|
||||||
|
permissions: NonNullable<TFormSchema["permissions"]>;
|
||||||
|
}>({
|
||||||
|
resolver: zodResolver(
|
||||||
|
formSchema.pick({ permissions: true }).extend({ type: z.nativeEnum(ProjectPermissionSub) })
|
||||||
|
),
|
||||||
|
defaultValues: {
|
||||||
|
type: ProjectPermissionSub.Secrets
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedSubject = form.watch("type");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="type"
|
||||||
|
defaultValue={ProjectPermissionSub.Secrets}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Subject" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{Object.keys(PROJECT_PERMISSION_OBJECT).map((subject) => (
|
||||||
|
<SelectItem value={subject} key={`permission-create-${subject}`}>
|
||||||
|
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormLabel label="Actions" className="my-2" />
|
||||||
|
<div className="flex flex-grow flex-wrap justify-start gap-8">
|
||||||
|
{PROJECT_PERMISSION_OBJECT?.[selectedSubject]?.actions?.map(({ label, value }) => (
|
||||||
|
<Controller
|
||||||
|
key={`create-permission-${selectedSubject}-${label}`}
|
||||||
|
name={`permissions.${selectedSubject}.0.${value as any}` as any}
|
||||||
|
control={form.control}
|
||||||
|
defaultValue={false}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
id={`new-permissions.${selectedSubject}.0.${String(value)}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex space-x-4">
|
||||||
|
<Button
|
||||||
|
onClick={form.handleSubmit((el) => {
|
||||||
|
const rootPolicyValue = rootForm.getValues("permissions")?.[el.type];
|
||||||
|
if (rootPolicyValue && selectedSubject === ProjectPermissionSub.Secrets) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||||
|
rootForm.setValue(`permissions.${el.type}`, [
|
||||||
|
...rootPolicyValue,
|
||||||
|
...(el?.permissions[el.type] || [])
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
|
||||||
|
rootForm.setValue(`permissions.${el.type}`, el?.permissions?.[el.type]);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
|
import { TFormSchema } from "../ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
position?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretPermissionConditions = ({ position = 0 }: Props) => {
|
||||||
|
const { control } = useFormContext<TFormSchema>();
|
||||||
|
const items = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `permissions.secrets.${position}.conditions`
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 border-t border-t-gray-800 bg-mineshaft-800 pt-2">
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
{items.fields.map((el, index) => (
|
||||||
|
<div
|
||||||
|
key={el.id}
|
||||||
|
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||||
|
>
|
||||||
|
<div className="w-1/4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.secrets.${position}.conditions.${index}.lhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value="environment">Environment</SelectItem>
|
||||||
|
<SelectItem value="secretPath">Secret Path</SelectItem>
|
||||||
|
<SelectItem value="secretName">Secret Name</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-36">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.secrets.${position}.conditions.${index}.operator`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$REGEX}>
|
||||||
|
Regex Match
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$ALL}>All</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.secrets.${position}.conditions.${index}.rhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="value" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="plus"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="p-2.5"
|
||||||
|
onClick={() => items.remove(index)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
variant="star"
|
||||||
|
size="xs"
|
||||||
|
className="mt-3"
|
||||||
|
onClick={() =>
|
||||||
|
items.append({
|
||||||
|
lhs: "environment",
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
rhs: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
New Condition
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user