diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 76e17cc48..63756a652 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -123,8 +123,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { .describe(SSH_HOSTS.CREATE.hostCertTtl), loginMappings: z .object({ - loginUser: z.string().describe(SSH_HOSTS.CREATE.loginUser), // TODO: reinforce validation - allowedPrincipals: z.array(z.string()).describe(SSH_HOSTS.CREATE.allowedPrincipals) // TODO: reinforce validation + loginUser: z.string().trim().describe(SSH_HOSTS.CREATE.loginUser), // TODO: reinforce validation + allowedPrincipals: z.array(z.string().trim()).describe(SSH_HOSTS.CREATE.allowedPrincipals) // TODO: reinforce validation }) .array() .default([]) @@ -207,8 +207,8 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { .describe(SSH_HOSTS.UPDATE.hostCertTtl), loginMappings: z .object({ - loginUser: z.string().describe(SSH_HOSTS.CREATE.loginUser), - allowedPrincipals: z.array(z.string()).describe(SSH_HOSTS.CREATE.allowedPrincipals) + loginUser: z.string().trim().describe(SSH_HOSTS.CREATE.loginUser), + allowedPrincipals: z.array(z.string().trim()).describe(SSH_HOSTS.CREATE.allowedPrincipals) }) .array() .optional() diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 231d22743..0ebeeb36c 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -67,6 +67,15 @@ export enum ProjectPermissionGroupActions { GrantPrivileges = "grant-privileges" } +export enum ProjectPermissionSshHostActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueUserCert = "issue-user-cert", + IssueHostCert = "issue-host-cert" +} + export enum ProjectPermissionSecretSyncActions { Read = "read", Create = "create", @@ -147,6 +156,10 @@ export type IdentityManagementSubjectFields = { identityId: string; }; +export type SshHostSubjectFields = { + hostname: string; +}; + export type ProjectPermissionSet = | [ ProjectPermissionSecretActions, @@ -196,7 +209,10 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] - | [ProjectPermissionActions, ProjectPermissionSub.SshHosts] + | [ + ProjectPermissionSshHostActions, + ProjectPermissionSub.SshHosts | (ForcedSubject & SshHostSubjectFields) + ] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] @@ -295,6 +311,21 @@ const IdentityManagementConditionSchema = z }) .partial(); +const SshHostConditionSchema = z + .object({ + hostname: z.union([ + z.string(), + z + .object({ + [PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ], + [PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB], + [PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN] + }) + .partial() + ]) + }) + .partial(); + const GeneralPermissionSchema = [ z.object({ subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), @@ -420,12 +451,6 @@ const GeneralPermissionSchema = [ "Describe what action an entity can take." ) }), - z.object({ - subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( - "Describe what action an entity can take." - ) - }), z.object({ subject: z.literal(ProjectPermissionSub.PkiAlerts).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( @@ -549,6 +574,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ "When specified, only matching conditions will be allowed to access given resource." ).optional() }), + z.object({ + subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSshHostActions).describe( + "Describe what action an entity can take." + ), + inverted: z.boolean().optional().describe("Whether rule allows or forbids."), + conditions: SshHostConditionSchema.describe( + "When specified, only matching conditions will be allowed to access given resource." + ).optional() + }), ...GeneralPermissionSchema ]); @@ -579,8 +614,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.PkiCollections, ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificates, - ProjectPermissionSub.SshCertificateTemplates, - ProjectPermissionSub.SshHosts + ProjectPermissionSub.SshCertificateTemplates ].forEach((el) => { can( [ @@ -593,6 +627,18 @@ const buildAdminPermissionRules = () => { ); }); + can( + [ + ProjectPermissionSshHostActions.Read, + ProjectPermissionSshHostActions.Edit, + ProjectPermissionSshHostActions.Create, + ProjectPermissionSshHostActions.Delete, + ProjectPermissionSshHostActions.IssueUserCert, + ProjectPermissionSshHostActions.IssueHostCert + ], + ProjectPermissionSub.SshHosts + ); + can( [ ProjectPermissionMemberActions.Create, @@ -840,7 +886,9 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates); can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates); - can([ProjectPermissionActions.Read], ProjectPermissionSub.SshHosts); + + can([ProjectPermissionSshHostActions.IssueUserCert], ProjectPermissionSub.SshHosts); + can( [ ProjectPermissionCmekActions.Create, @@ -901,7 +949,6 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); - can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); return rules; diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index 586c3b53c..96c7b2ae1 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -1,8 +1,8 @@ -import { ForbiddenError } from "@casl/ability"; +import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, ProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionSshHostActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal"; @@ -63,11 +63,6 @@ type TSshCertificateAuthorityServiceFactoryDep = { export type TSshHostServiceFactory = ReturnType; -/** - * Checklist: - * - check permissions across various roles to make sure it makes sense for the SSH hosts - */ - export const sshHostServiceFactory = ({ userDAL, projectDAL, @@ -82,7 +77,8 @@ export const sshHostServiceFactory = ({ kmsService }: TSshCertificateAuthorityServiceFactoryDep) => { /** - * Return list of all SSH hosts that a user has access to across all SSH projects in the organization + * Return list of all SSH hosts that a user can issue user SSH certificates for + * (i.e. is able to access / connect to) across all SSH projects in the organization */ const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => { const sshProjects = await projectDAL.find({ @@ -90,11 +86,15 @@ export const sshHostServiceFactory = ({ type: ProjectType.SSH }); - const projectIdsWithAccess: string[] = []; + const principals = await convertActorToPrincipals({ + actor, + actorId, + userDAL + }); + + const allAllowedHosts = []; for await (const project of sshProjects) { - let hasAccess = false; - try { const { permission } = await permissionService.getProjectPermission({ actor, @@ -105,26 +105,22 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); - // TODO: consider glob-based permission items - hasAccess = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); - } catch { - hasAccess = false; - } + const projectHosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects([project.id], principals); - if (hasAccess) { - projectIdsWithAccess.push(project.id); + const allowedHosts = projectHosts.filter((host) => + permission.can( + ProjectPermissionSshHostActions.IssueUserCert, + subject(ProjectPermissionSub.SshHosts, { hostname: host.hostname }) + ) + ); + + allAllowedHosts.push(...allowedHosts); + } catch { + // intentionally ignore projects where user lacks access } } - const principals = await convertActorToPrincipals({ - actor, - actorId, - userDAL - }); - - const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, principals); - - return hosts; + return allAllowedHosts; }; const createSshHost = async ({ @@ -149,7 +145,12 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHosts); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Create, + subject(ProjectPermissionSub.SshHosts, { + hostname + }) + ); const resolveSshCaId = async ({ requestedId, @@ -257,7 +258,12 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SshHosts); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Edit, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); const updatedHost = await sshHostDAL.transaction(async (tx) => { await sshHostDAL.updateById( @@ -308,7 +314,12 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.SshHosts); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Delete, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); await sshHostDAL.transaction(async (tx) => { await sshHostLoginMappingDAL.delete({ sshHostId }, tx); @@ -335,7 +346,12 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.Read, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); return host; }; @@ -370,9 +386,12 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); - - // TODO: update permissions + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.IssueUserCert, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); const keyId = `${actor}-${actorId}`; @@ -488,8 +507,12 @@ export const sshHostServiceFactory = ({ actionProjectType: ActionProjectType.SSH }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts); - // TODO: update permissions + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionSshHostActions.IssueHostCert, + subject(ProjectPermissionSub.SshHosts, { + hostname: host.hostname + }) + ); const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId }); diff --git a/backend/src/services/project-role/project-role-service.ts b/backend/src/services/project-role/project-role-service.ts index 3dfe11d2c..fc2fb9319 100644 --- a/backend/src/services/project-role/project-role-service.ts +++ b/backend/src/services/project-role/project-role-service.ts @@ -141,6 +141,7 @@ export const projectRoleServiceFactory = ({ validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), { allowedExpressions: (val) => val.includes("identity.") }); + const updatedRole = await projectRoleDAL.updateById(projectRole.id, { ...data, permissions: data.permissions ? data.permissions : undefined diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 2312e89a8..bebf73218 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -75,6 +75,15 @@ export enum ProjectPermissionGroupActions { GrantPrivileges = "grant-privileges" } +export enum ProjectPermissionSshHostActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + IssueUserCert = "issue-user-cert", + IssueHostCert = "issue-host-cert" +} + export enum PermissionConditionOperators { $IN = "$in", $ALL = "$all", @@ -225,7 +234,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] - | [ProjectPermissionActions, ProjectPermissionSub.SshHosts] + | [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs] diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 9bbf61dfa..3b7f9a04b 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -18,6 +18,7 @@ import { ProjectPermissionMemberActions, ProjectPermissionSecretActions, ProjectPermissionSecretSyncActions, + ProjectPermissionSshHostActions, TPermissionCondition, TPermissionConditionOperators } from "@app/context/ProjectPermissionContext/types"; @@ -98,6 +99,15 @@ const GroupPolicyActionSchema = z.object({ [ProjectPermissionGroupActions.GrantPrivileges]: z.boolean().optional() }); +const SshHostPolicyActionSchema = z.object({ + [ProjectPermissionSshHostActions.Read]: z.boolean().optional(), + [ProjectPermissionSshHostActions.Create]: z.boolean().optional(), + [ProjectPermissionSshHostActions.Edit]: z.boolean().optional(), + [ProjectPermissionSshHostActions.Delete]: z.boolean().optional(), + [ProjectPermissionSshHostActions.IssueUserCert]: z.boolean().optional(), + [ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional() +}); + const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -205,7 +215,12 @@ export const projectRoleFormSchema = z.object({ ), [ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]), - [ProjectPermissionSub.SshHosts]: GeneralPolicyActionSchema.array().default([]), + [ProjectPermissionSub.SshHosts]: SshHostPolicyActionSchema.extend({ + inverted: z.boolean().optional(), + conditions: ConditionSchema + }) + .array() + .default([]), [ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]), [ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]), @@ -227,7 +242,8 @@ type TConditionalFields = | ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretImports | ProjectPermissionSub.DynamicSecrets - | ProjectPermissionSub.Identity; + | ProjectPermissionSub.Identity + | ProjectPermissionSub.SshHosts; export const isConditionalSubjects = ( subject: ProjectPermissionSub @@ -236,7 +252,8 @@ export const isConditionalSubjects = ( subject === ProjectPermissionSub.DynamicSecrets || subject === ProjectPermissionSub.SecretImports || subject === ProjectPermissionSub.SecretFolders || - subject === ProjectPermissionSub.Identity; + subject === ProjectPermissionSub.Identity || + subject === ProjectPermissionSub.SshHosts; const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const formConditions: z.infer = []; @@ -293,7 +310,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.Tags, ProjectPermissionSub.SecretRotation, ProjectPermissionSub.Kms, - ProjectPermissionSub.SshHosts, ProjectPermissionSub.SshCertificateTemplates, ProjectPermissionSub.SshCertificateAuthorities, ProjectPermissionSub.SshCertificates @@ -541,7 +557,33 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { if (canRemoveSecrets) formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true; } + + if (subject === ProjectPermissionSub.SshHosts) { + const canRead = action.includes(ProjectPermissionSshHostActions.Read); + const canEdit = action.includes(ProjectPermissionSshHostActions.Edit); + const canDelete = action.includes(ProjectPermissionSshHostActions.Delete); + const canCreate = action.includes(ProjectPermissionSshHostActions.Create); + const canIssueUserCert = action.includes(ProjectPermissionSshHostActions.IssueUserCert); + const canIssueHostCert = action.includes(ProjectPermissionSshHostActions.IssueHostCert); + + if (!formVal[subject]) formVal[subject] = [{ conditions: [] }]; + + if (canRead) formVal[subject]![0][ProjectPermissionSshHostActions.Read] = true; + if (canEdit) formVal[subject]![0][ProjectPermissionSshHostActions.Edit] = true; + if (canDelete) formVal[subject]![0][ProjectPermissionSshHostActions.Delete] = true; + if (canCreate) formVal[subject]![0][ProjectPermissionSshHostActions.Create] = true; + if (canIssueUserCert) + formVal[subject]![0][ProjectPermissionSshHostActions.IssueUserCert] = true; + if (canIssueHostCert) + formVal[subject]![0][ProjectPermissionSshHostActions.IssueHostCert] = true; + + formVal[subject]![0].conditions = conditions + ? convertCaslConditionToFormOperator(conditions) + : []; + formVal[subject]![0].inverted = inverted; + } }); + return formVal; }; @@ -868,10 +910,12 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.SshHosts]: { title: "SSH Hosts", actions: [ - { label: "Read", value: "read" }, - { label: "Create", value: "create" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { label: "Read", value: ProjectPermissionSshHostActions.Read }, + { label: "Create", value: ProjectPermissionSshHostActions.Create }, + { label: "Modify", value: ProjectPermissionSshHostActions.Edit }, + { label: "Remove", value: ProjectPermissionSshHostActions.Delete }, + { label: "Connect", value: ProjectPermissionSshHostActions.IssueUserCert }, + { label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert } ] }, [ProjectPermissionSub.PkiCollections]: { diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx index d44927edf..5b2dc65f2 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection.tsx @@ -19,6 +19,7 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { evaluatePermissionsAbility } from "@app/helpers/permissions"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { GeneralPermissionConditions } from "./GeneralPermissionConditions"; import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies"; @@ -33,6 +34,7 @@ import { TFormSchema } from "./ProjectRoleModifySection.utils"; import { SecretPermissionConditions } from "./SecretPermissionConditions"; +import { SshHostPermissionConditions } from "./SshHostPermissionConditions"; type Props = { roleSlug: string; @@ -51,6 +53,10 @@ export const renderConditionalComponents = ( return ; } + if (subject === ProjectPermissionSub.SshHosts) { + return ; + } + return ; } @@ -134,7 +140,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { return (
- + {currentWorkspace.type === ProjectType.SecretManager && ( + + )}
{ + const { + control, + watch, + formState: { errors } + } = useFormContext(); + + const permissionSubject = ProjectPermissionSub.SshHosts; + const items = useFieldArray({ + control, + name: `permissions.${permissionSubject}.${position}.conditions` + }); + + return ( +
+

Conditions

+

+ Conditions determine when a policy will be applied (always if no conditions are present). +

+

+ All conditions must evaluate to true for the policy to take effect. +

+
+ {items.fields.map((el, index) => { + const condition = + (watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as { + lhs: string; + rhs: string; + operator: string; + }) || {}; + + return ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> + + + +
+
+ ( + + + + )} + /> +
+
+ items.remove(index)} + > + + +
+
+ ); + })} +
+ {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && ( +
+ + {errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message} +
+ )} +
+ +
+
+ ); +};