feat: added identity id condition in identity permission of a project

This commit is contained in:
=
2024-12-04 21:26:23 +05:30
parent 5ceb30f43f
commit 3c63312944
16 changed files with 481 additions and 102 deletions
@@ -1,4 +1,4 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
@@ -69,7 +69,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityId, identityId,
@@ -146,7 +150,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -241,7 +249,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission( const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityProjectMembership.identityId, identityProjectMembership.identityId,
@@ -294,7 +306,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -333,7 +348,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
projectMembershipId: identityProjectMembership.id projectMembershipId: identityProjectMembership.id
@@ -82,6 +82,10 @@ export type SecretImportSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type IdentityManagementSubjectFields = {
identityId: string;
};
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
@@ -121,7 +125,10 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity] | [
ProjectPermissionActions,
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
@@ -213,6 +220,21 @@ const SecretConditionV2Schema = z
}) })
.partial(); .partial();
const IdentityManagementConditionSchema = z
.object({
identityId: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
@@ -262,12 +284,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
@@ -373,6 +389,12 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);
@@ -417,6 +439,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: IdentityManagementConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);
@@ -697,26 +729,26 @@ export const buildServiceTokenProjectPermission = (
[ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach(
(subject) => { (subject) => {
if (canWrite) { if (canWrite) {
// TODO: @Akhi
// @ts-expect-error type
can(ProjectPermissionActions.Edit, subject, { can(ProjectPermissionActions.Edit, subject, {
// TODO: @Akhi
// @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
// @ts-expect-error type
can(ProjectPermissionActions.Create, subject, { can(ProjectPermissionActions.Create, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
// @ts-expect-error type
can(ProjectPermissionActions.Delete, subject, { can(ProjectPermissionActions.Delete, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
} }
if (canRead) { if (canRead) {
// @ts-expect-error type
can(ProjectPermissionActions.Read, subject, { can(ProjectPermissionActions.Read, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
@@ -161,7 +161,10 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId });
if (!projectIdentity) if (!projectIdentity)
@@ -253,7 +256,11 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission( const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityId, identityId,
@@ -317,7 +324,11 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId }); const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId });
if (!identityMembership) if (!identityMembership)
+1 -1
View File
@@ -89,7 +89,7 @@
"react-mailchimp-subscribe": "^2.1.3", "react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-select": "^5.8.3", "react-select": "^5.8.1",
"react-table": "^7.8.0", "react-table": "^7.8.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.12.1",
@@ -33,6 +33,10 @@ export enum PermissionConditionOperators {
$GLOB = "$glob" $GLOB = "$glob"
} }
export type IdentityManagementSubjectFields = {
identityId: string;
};
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to", [PermissionConditionOperators.$EQ]: "equal to",
[PermissionConditionOperators.$IN]: "contains", [PermissionConditionOperators.$IN]: "contains",
@@ -151,7 +155,13 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity] | [
ProjectPermissionActions,
(
| ProjectPermissionSub.Identity
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
+1 -1
View File
@@ -92,7 +92,7 @@ export const queryClient = new QueryClient({
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{serverResponse.details?.map((el, index) => { {serverResponse.details?.map((el, index) => {
const hasConditions = Object.keys(el.conditions || {}).length; const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
return ( return (
<div <div
key={`Forbidden-error-details-${index + 1}`} key={`Forbidden-error-details-${index + 1}`}
@@ -1,4 +1,5 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns"; import { format } from "date-fns";
@@ -94,7 +95,9 @@ export const IdentityDetailsPage = withProjectPermission(
<div> <div>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Identity} a={subject(ProjectPermissionSub.Identity, {
identityId: identityMembershipDetails?.identity?.id
})}
renderTooltip renderTooltip
allowedLabel="Remove from project" allowedLabel="Remove from project"
> >
@@ -17,25 +17,24 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
Modal,
ModalContent,
ModalTrigger,
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
Tag, Tag,
Tooltip Tooltip} from "@app/components/v2";
} from "@app/components/v2";
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateIdentityProjectAdditionalPrivilege, useCreateIdentityProjectAdditionalPrivilege,
useGetIdentityProjectPrivilegeDetails, useGetIdentityProjectPrivilegeDetails,
@@ -43,14 +42,13 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies";
import { NewPermissionRule } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule";
import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState"; import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState";
import { import {
formRolePermission2API, formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema, projectRoleFormSchema,
rolePermission2Form rolePermission2Form} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection"; import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection";
type Props = { type Props = {
@@ -88,7 +86,6 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
}: Props) => { }: Props) => {
const isCreate = !privilegeId; const isCreate = !privilegeId;
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
const { data: privilegeDetails, isLoading } = useGetIdentityProjectPrivilegeDetails({ const { data: privilegeDetails, isLoading } = useGetIdentityProjectPrivilegeDetails({
identityId, identityId,
@@ -194,6 +191,30 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
} }
} }
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
return ( return (
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@@ -232,24 +253,39 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
> >
Save Save
</Button> </Button>
<Modal <DropdownMenu>
isOpen={popUp.createPolicy.isOpen} <DropdownMenuTrigger>
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
>
<ModalTrigger asChild>
<Button <Button
isDisabled={isDisabled}
className="h-10 rounded-l-none" className="h-10 rounded-l-none"
variant="outline_bg" variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={isDisabled}
> >
New Policy New policy
</Button> </Button>
</ModalTrigger> </DropdownMenuTrigger>
<ModalContent title="New Policy" subTitle="Policies grant additional permissions."> <DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} /> {Object.keys(PROJECT_PERMISSION_OBJECT)
</ModalContent> .sort((a, b) =>
</Modal> PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faEllipsisV, faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faEllipsisV, faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format, formatDistance } from "date-fns"; import { format, formatDistance } from "date-fns";
@@ -83,7 +84,9 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id} privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
isDisabled={permission.cannot( isDisabled={permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
ProjectPermissionSub.Identity subject(ProjectPermissionSub.Identity, {
identityId
})
)} )}
/> />
</motion.div> </motion.div>
@@ -103,7 +106,9 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Identity} a={subject(ProjectPermissionSub.Identity, {
identityId
})}
renderTooltip renderTooltip
allowedLabel="Add Privilege" allowedLabel="Add Privilege"
> >
@@ -192,7 +197,9 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100"> <div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Identity} a={subject(ProjectPermissionSub.Identity, {
identityId
})}
renderTooltip renderTooltip
allowedLabel="Remove Role" allowedLabel="Remove Role"
> >
@@ -17,12 +17,13 @@ import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
Button, Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl, FormControl,
FormLabel, FormLabel,
Input, Input,
Modal,
ModalContent,
ModalTrigger,
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
@@ -35,7 +36,6 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { usePopUp } from "@app/hooks";
import { import {
useCreateProjectUserAdditionalPrivilege, useCreateProjectUserAdditionalPrivilege,
useGetProjectUserPrivilegeDetails, useGetProjectUserPrivilegeDetails,
@@ -43,14 +43,13 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types";
import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies";
import { NewPermissionRule } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule";
import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState"; import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState";
import { import {
formRolePermission2API, formRolePermission2API,
isConditionalSubjects,
PROJECT_PERMISSION_OBJECT, PROJECT_PERMISSION_OBJECT,
projectRoleFormSchema, projectRoleFormSchema,
rolePermission2Form rolePermission2Form} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection"; import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection";
type Props = { type Props = {
@@ -88,7 +87,6 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
}: Props) => { }: Props) => {
const isCreate = !privilegeId; const isCreate = !privilegeId;
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
const { data: privilegeDetails, isLoading } = useGetProjectUserPrivilegeDetails( const { data: privilegeDetails, isLoading } = useGetProjectUserPrivilegeDetails(
privilegeId || "" privilegeId || ""
@@ -167,6 +165,30 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
} }
}; };
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
const privilegeTemporaryAccess = form.watch("temporaryAccess"); const privilegeTemporaryAccess = form.watch("temporaryAccess");
const isTemporary = privilegeTemporaryAccess?.isTemporary; const isTemporary = privilegeTemporaryAccess?.isTemporary;
const isExpired = const isExpired =
@@ -229,24 +251,39 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
> >
Save Save
</Button> </Button>
<Modal <DropdownMenu>
isOpen={popUp.createPolicy.isOpen} <DropdownMenuTrigger>
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
>
<ModalTrigger asChild>
<Button <Button
isDisabled={isDisabled}
className="h-10 rounded-l-none" className="h-10 rounded-l-none"
variant="outline_bg" variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={isDisabled}
> >
New Policy New policy
</Button> </Button>
</ModalTrigger> </DropdownMenuTrigger>
<ModalContent title="New Policy" subTitle="Policies grant additional permissions."> <DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} /> {Object.keys(PROJECT_PERMISSION_OBJECT)
</ModalContent> .sort((a, b) =>
</Modal> PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title
.toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
</div> </div>
@@ -1,5 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { subject } from "@casl/ability";
import { import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
@@ -349,7 +350,9 @@ export const IdentityTab = withProjectPermission(
<Td className="flex justify-end space-x-2 opacity-0 duration-300 group-hover:opacity-100"> <Td className="flex justify-end space-x-2 opacity-0 duration-300 group-hover:opacity-100">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Delete} I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.Identity} a={subject(ProjectPermissionSub.Identity, {
identityId: id
})}
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
@@ -105,9 +105,14 @@ export const projectRoleFormSchema = z.object({
}) })
.array() .array()
.default([]), .default([]),
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]),
@@ -139,7 +144,8 @@ type TConditionalFields =
| ProjectPermissionSub.Secrets | ProjectPermissionSub.Secrets
| ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretImports
| ProjectPermissionSub.DynamicSecrets; | ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.Identity;
export const isConditionalSubjects = ( export const isConditionalSubjects = (
subject: ProjectPermissionSub subject: ProjectPermissionSub
@@ -147,7 +153,8 @@ export const isConditionalSubjects = (
subject === (ProjectPermissionSub.Secrets as const) || subject === (ProjectPermissionSub.Secrets as const) ||
subject === ProjectPermissionSub.DynamicSecrets || subject === ProjectPermissionSub.DynamicSecrets ||
subject === ProjectPermissionSub.SecretImports || subject === ProjectPermissionSub.SecretImports ||
subject === ProjectPermissionSub.SecretFolders; subject === ProjectPermissionSub.SecretFolders ||
subject === ProjectPermissionSub.Identity;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = []; const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -483,8 +490,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Remove members", value: "delete" } { label: "Remove members", value: "delete" }
] ]
}, },
[ProjectPermissionSub.Groups]: { [ProjectPermissionSub.Identity]: {
title: "Group Management", title: "Machine Identity Management",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: "read" },
{ label: "Create", value: "create" }, { label: "Create", value: "create" },
@@ -492,8 +499,8 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Remove", value: "delete" } { label: "Remove", value: "delete" }
] ]
}, },
[ProjectPermissionSub.Identity]: { [ProjectPermissionSub.Groups]: {
title: "Machine Identity Management", title: "Group Management",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: "read" },
{ label: "Create", value: "create" }, { label: "Create", value: "create" },
@@ -527,7 +534,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
] ]
}, },
[ProjectPermissionSub.Environments]: { [ProjectPermissionSub.Environments]: {
title: "Environments", title: "Environment Management",
actions: [ actions: [
{ label: "Read", value: "read" }, { label: "Read", value: "read" },
{ label: "Create", value: "create" }, { label: "Create", value: "create" },
@@ -5,14 +5,19 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Alert, Button, Modal, ModalContent, ModalTrigger } from "@app/components/v2"; import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} 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 { GeneralPermissionConditions } from "./components/GeneralPermissionConditions"; import { GeneralPermissionConditions } from "./components/GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies"; import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies";
import { NewPermissionRule } from "./components/NewPermissionRule"; import { IdentityManagementPermissionConditions } from "./components/IdentityManagementPermissionConditions";
import { SecretPermissionConditions } from "./components/SecretPermissionConditions"; import { SecretPermissionConditions } from "./components/SecretPermissionConditions";
import { PermissionEmptyState } from "./PermissionEmptyState"; import { PermissionEmptyState } from "./PermissionEmptyState";
import { import {
@@ -37,6 +42,10 @@ export const renderConditionalComponents = (
return <SecretPermissionConditions isDisabled={isDisabled} />; return <SecretPermissionConditions isDisabled={isDisabled} />;
if (isConditionalSubjects(subject)) { if (isConditionalSubjects(subject)) {
if (subject === ProjectPermissionSub.Identity) {
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />; return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
} }
@@ -45,7 +54,6 @@ export const renderConditionalComponents = (
export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
const { data: role, isLoading } = useGetProjectRoleBySlug( const { data: role, isLoading } = useGetProjectRoleBySlug(
currentWorkspace?.id ?? "", currentWorkspace?.id ?? "",
@@ -83,6 +91,30 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? ""); const isCustomRole = !["admin", "member", "viewer", "no-access"].includes(role?.slug ?? "");
const onNewPolicy = (selectedSubject: ProjectPermissionSub) => {
const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`);
if (rootPolicyValue && isConditionalSubjects(selectedSubject)) {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[...rootPolicyValue, ...[]],
{ shouldDirty: true, shouldTouch: true }
);
} else {
form.setValue(
`permissions.${selectedSubject}`,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error akhilmhdh: this is because of ts collision with both
[{}],
{
shouldDirty: true,
shouldTouch: true
}
);
}
};
return ( return (
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@@ -116,11 +148,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
> >
Save Save
</Button> </Button>
<Modal <DropdownMenu>
isOpen={popUp.createPolicy.isOpen} <DropdownMenuTrigger>
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
>
<ModalTrigger asChild disabled={isDisabled}>
<Button <Button
isDisabled={isDisabled} isDisabled={isDisabled}
className="h-10 rounded-l-none" className="h-10 rounded-l-none"
@@ -129,23 +158,36 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
> >
New policy New policy
</Button> </Button>
</ModalTrigger> </DropdownMenuTrigger>
<ModalContent <DropdownMenuContent className="thin-scrollbar max-h-96" align="end">
title="New Policy" {Object.keys(PROJECT_PERMISSION_OBJECT)
subTitle="Policies grant additional permissions." .sort((a, b) =>
> PROJECT_PERMISSION_OBJECT[
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} /> a as keyof typeof PROJECT_PERMISSION_OBJECT
</ModalContent> ].title
</Modal> .toLowerCase()
.localeCompare(
PROJECT_PERMISSION_OBJECT[
b as keyof typeof PROJECT_PERMISSION_OBJECT
].title.toLowerCase()
)
)
.map((subject) => (
<DropdownMenuItem
key={`permission-create-${subject}`}
className="py-3"
onClick={() => onNewPolicy(subject as ProjectPermissionSub)}
>
{PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</> </>
)} )}
</div> </div>
</div> </div>
<Alert
title="Infisical's permissions are now more granular. You can set separate permissions for folders, dynamic secrets, and imports. No changes required for existing APIs and clients."
className="mt-4 border-primary/50 bg-primary/10"
/>
<div className="py-4"> <div className="py-4">
{!isLoading && <PermissionEmptyState />} {!isLoading && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
@@ -157,7 +157,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
size="xs" size="xs"
className="mt-2" className="mt-2"
onClick={() => { onClick={() => {
items.insert(rootIndex, [ items.insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any { read: false, edit: false, create: false, delete: false } as any
]); ]);
}} }}
@@ -0,0 +1,171 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { TFormSchema } from "../ProjectRoleModifySection.utils";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.Identity;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="mb-2 text-sm text-mineshaft-400">
When this policy should apply (always if no conditions are added).
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<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.${permissionSubject}.${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="identityId">Identity ID</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${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.$IN}>Contains</SelectItem>
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "identityId",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};
@@ -21,6 +21,7 @@ export const renderOperatorSelectItems = (type: string) => {
if (type === "secretTags") { if (type === "secretTags") {
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>; return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
} }
return ( return (
<> <>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem> <SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>