mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: updated ui for new permission
This commit is contained in:
@@ -7,6 +7,30 @@ export enum ProjectPermissionActions {
|
||||
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 {
|
||||
Role = "role",
|
||||
Member = "member",
|
||||
|
||||
@@ -40,7 +40,7 @@ export type TPermission = {
|
||||
|
||||
export type TProjectPermission = {
|
||||
conditions?: Record<string, any>;
|
||||
action: string;
|
||||
action: string | string[];
|
||||
subject: string | string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
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";
|
||||
|
||||
const generalPermissionSchema = z
|
||||
.object({
|
||||
read: z.boolean().optional(),
|
||||
edit: z.boolean().optional(),
|
||||
delete: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
const GeneralPolicyActionSchema = z.object({
|
||||
read: z.boolean().optional(),
|
||||
edit: z.boolean().optional(),
|
||||
delete: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
});
|
||||
|
||||
const multiEnvPermissionSchema = z
|
||||
.object({
|
||||
secretPath: z.string().trim().optional(),
|
||||
read: z.boolean().optional(),
|
||||
edit: z.boolean().optional(),
|
||||
delete: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
const SecretFolderPolicyActionSchema = z.object({
|
||||
read: z.boolean().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({
|
||||
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" }),
|
||||
permissions: z
|
||||
.object({
|
||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||
"secret-folders": generalPermissionSchema.optional(),
|
||||
member: generalPermissionSchema,
|
||||
groups: generalPermissionSchema,
|
||||
identity: generalPermissionSchema,
|
||||
role: generalPermissionSchema,
|
||||
integrations: generalPermissionSchema,
|
||||
webhooks: generalPermissionSchema,
|
||||
"service-tokens": generalPermissionSchema,
|
||||
settings: generalPermissionSchema,
|
||||
environments: generalPermissionSchema,
|
||||
tags: generalPermissionSchema,
|
||||
"ip-allowlist": generalPermissionSchema,
|
||||
"certificate-authorities": generalPermissionSchema,
|
||||
certificates: generalPermissionSchema,
|
||||
"pki-alerts": generalPermissionSchema,
|
||||
"pki-collections": generalPermissionSchema,
|
||||
"certificate-templates": generalPermissionSchema,
|
||||
// akhilmhdh: refactor all keys like below
|
||||
[ProjectPermissionSub.SecretApproval]: generalPermissionSchema,
|
||||
workspace: z
|
||||
.object({
|
||||
edit: z.boolean().optional(),
|
||||
delete: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
"secret-rollback": z
|
||||
.object({
|
||||
read: z.boolean().optional(),
|
||||
create: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
[ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({
|
||||
conditions: ConditionSchema.array().optional().default([])
|
||||
})
|
||||
.array()
|
||||
.default([]),
|
||||
[ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.ServiceTokens]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Settings]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Certificates]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]),
|
||||
[ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([])
|
||||
})
|
||||
.partial()
|
||||
.optional()
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
const multiEnvApi2Form = (
|
||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
||||
permission: TProjectPermission
|
||||
) => {
|
||||
const isCustomRule = Boolean(permission?.conditions?.environment);
|
||||
// full access
|
||||
if (isCustomRule && formVal && !formVal?.custom) {
|
||||
formVal.custom = { read: true, edit: true, delete: true, create: true };
|
||||
}
|
||||
|
||||
const secretEnv = permission?.conditions?.environment || "all";
|
||||
const secretPath = permission?.conditions?.secretPath?.$glob;
|
||||
// initialize
|
||||
if (formVal && !formVal?.[secretEnv]) {
|
||||
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
||||
}
|
||||
|
||||
formVal[secretEnv][permission.action] = true;
|
||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||
const formConditions: z.infer<typeof ConditionSchema>[] = [];
|
||||
Object.entries(caslConditions).forEach(([type, condition]) => {
|
||||
if (typeof condition === "string") {
|
||||
formConditions.push({
|
||||
operator: PermissionConditionOperators.$EQ,
|
||||
lhs: type,
|
||||
rhs: condition
|
||||
});
|
||||
} else {
|
||||
Object.keys(condition).forEach((conditionOperator) => {
|
||||
const rhs = condition[conditionOperator as PermissionConditionOperators];
|
||||
formConditions.push({
|
||||
operator: conditionOperator,
|
||||
lhs: type,
|
||||
rhs: typeof rhs === "string" ? rhs : rhs.join(",")
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
return formConditions;
|
||||
};
|
||||
|
||||
// convert role permission to form compatiable data structure
|
||||
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||
// any because if it set it as form type due to the discriminated union type of ts
|
||||
// i would have to write a if loop with both conditions same
|
||||
const formVal: Record<string, any> = {};
|
||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
||||
|
||||
permissions.forEach((permission) => {
|
||||
const { subject: caslSub, action } = permission;
|
||||
const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
|
||||
if (!formVal?.[subject]) formVal[subject] = {};
|
||||
const { subject: caslSub, action, conditions } = permission;
|
||||
const subject = (typeof caslSub === "string" ? caslSub : caslSub[0]) as ProjectPermissionSub;
|
||||
|
||||
if (subject === "secrets") {
|
||||
multiEnvApi2Form(formVal[subject], permission);
|
||||
} else {
|
||||
// everything else follows same pattern
|
||||
// formVal[settings][read | write] = true
|
||||
formVal[subject][action] = true;
|
||||
if (
|
||||
[
|
||||
ProjectPermissionSub.Secrets,
|
||||
ProjectPermissionSub.Member,
|
||||
ProjectPermissionSub.Groups,
|
||||
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;
|
||||
};
|
||||
|
||||
const multiEnvForm2Api = (
|
||||
permissions: TProjectPermission[],
|
||||
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
||||
subject: "secrets"
|
||||
const convertFormOperatorToCaslCondition = (
|
||||
conditions: { lhs: string; rhs: string; operator: string }[]
|
||||
) => {
|
||||
if (!formVal) return;
|
||||
|
||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||
// if any of them is set in all push it without any condition
|
||||
PERMISSION_ACTIONS.forEach((action) => {
|
||||
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
||||
const caslCondition: Record<string, Partial<TPermissionConditionOperators>> = {};
|
||||
conditions.forEach((el) => {
|
||||
if (!caslCondition[el.lhs]) caslCondition[el.lhs] = {};
|
||||
if (
|
||||
el.operator === PermissionConditionOperators.$IN ||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return caslCondition;
|
||||
};
|
||||
|
||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||
const permissions: TProjectPermission[] = [];
|
||||
// other than workspace everything else follows same
|
||||
// if in future there is a different follow the above on how workspace is done
|
||||
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||
if (rule === "secrets") {
|
||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule);
|
||||
} else if (actions) {
|
||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||
if (isAllowed) {
|
||||
permissions.push({ subject: rule, action });
|
||||
}
|
||||
Object.entries(formVal || {}).forEach(([subject, rules]) => {
|
||||
rules.forEach((actions) => {
|
||||
const caslActions = Object.keys(actions).filter(
|
||||
(el) => actions?.[el as keyof typeof actions]
|
||||
);
|
||||
const caslConditions =
|
||||
"conditions" in actions
|
||||
? convertFormOperatorToCaslCondition(actions.conditions)
|
||||
: undefined;
|
||||
|
||||
permissions.push({
|
||||
action: caslActions,
|
||||
subject: [subject],
|
||||
conditions: caslConditions
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
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" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 { AnimatePresence } from "framer-motion";
|
||||
|
||||
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 { usePopUp } from "@app/hooks";
|
||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||
|
||||
import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions";
|
||||
import { NewPermissionRule } from "./components/NewPermissionRule";
|
||||
import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
|
||||
import {
|
||||
formRolePermission2API,
|
||||
formSchema,
|
||||
PROJECT_PERMISSION_OBJECT,
|
||||
rolePermission2Form,
|
||||
TFormSchema
|
||||
} from "@app/views/Project/RolePage/components/RolePermissionsSection/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;
|
||||
} from "./ProjectRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
roleSlug: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
||||
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
||||
const projectSlug = currentWorkspace?.slug || "";
|
||||
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 {
|
||||
setValue,
|
||||
getValues,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isSubmitting },
|
||||
reset
|
||||
} = useForm<TFormSchema>({
|
||||
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
} = form;
|
||||
|
||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||
|
||||
const onSubmit = async (el: TFormSchema) => {
|
||||
try {
|
||||
if (!projectSlug || !role?.id) return;
|
||||
|
||||
await updateRole({
|
||||
id: role?.id as string,
|
||||
projectSlug,
|
||||
@@ -143,70 +68,63 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => {
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
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">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Permissions</h3>
|
||||
{isCustomRole && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isCustomRole && (
|
||||
<>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-5" />
|
||||
<Th>Resource</Th>
|
||||
<Th>Permission</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
<RowPermissionSecretsRow
|
||||
title="Secrets"
|
||||
formName={ProjectPermissionSub.Secrets}
|
||||
isEditable={isCustomRole}
|
||||
setValue={setValue}
|
||||
getValue={getValues}
|
||||
control={control}
|
||||
/>
|
||||
<RowPermissionSecretFoldersRow
|
||||
isEditable={isCustomRole}
|
||||
setValue={setValue}
|
||||
control={control}
|
||||
/>
|
||||
{SINGLE_PERMISSION_LIST.map((permission) => {
|
||||
return (
|
||||
<RolePermissionRow
|
||||
title={permission.title}
|
||||
formName={permission.formName}
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
key={`project-role-${roleSlug}-permission-${permission.formName}`}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
<div className="py-4">
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionOptions
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
>
|
||||
{subject === ProjectPermissionSub.Secrets ? (
|
||||
<SecretPermissionConditions />
|
||||
) : undefined}
|
||||
</GeneralPermissionOptions>
|
||||
))}
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
<Modal
|
||||
isOpen={popUp.createPolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
||||
>
|
||||
<ModalTrigger asChild disabled={isDisabled}>
|
||||
<Button isDisabled={isDisabled} leftIcon={<FontAwesomeIcon icon={faPlus} />}>
|
||||
Add Policy
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent title="New Policy" subTitle="Policies grant additional permissions.">
|
||||
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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