mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 15:40:47 +00:00
Merge pull request #2838 from akhilmhdh/feat/identity-management-condition
feat: added identity id condition in identity permission of a project
This commit is contained in:
+25
-7
@@ -1,4 +1,4 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import { packRules } from "@casl/ability/extra";
|
import { packRules } from "@casl/ability/extra";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
@@ -62,7 +62,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
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,
|
||||||
@@ -139,7 +142,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||||
|
);
|
||||||
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
|
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
|
||||||
ActorType.IDENTITY,
|
ActorType.IDENTITY,
|
||||||
identityProjectMembership.identityId,
|
identityProjectMembership.identityId,
|
||||||
@@ -216,7 +222,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||||
|
);
|
||||||
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
|
const { permission: identityRolePermission } = await permissionService.getProjectPermission(
|
||||||
ActorType.IDENTITY,
|
ActorType.IDENTITY,
|
||||||
identityProjectMembership.identityId,
|
identityProjectMembership.identityId,
|
||||||
@@ -258,7 +267,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...identityPrivilege,
|
...identityPrivilege,
|
||||||
@@ -289,7 +301,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||||
|
);
|
||||||
|
|
||||||
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||||
slug,
|
slug,
|
||||||
@@ -321,7 +336,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||||
|
);
|
||||||
|
|
||||||
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find(
|
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find(
|
||||||
{
|
{
|
||||||
|
|||||||
+25
-6
@@ -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";
|
||||||
@@ -61,7 +61,12 @@ export const identityProjectServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
subject(ProjectPermissionSub.Identity, {
|
||||||
|
identityId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId });
|
const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId });
|
||||||
if (existingIdentity)
|
if (existingIdentity)
|
||||||
@@ -161,7 +166,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 +261,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 +329,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)
|
||||||
|
|||||||
Generated
+1
-1
@@ -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]
|
||||||
|
|||||||
@@ -93,7 +93,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"
|
||||||
>
|
>
|
||||||
|
|||||||
+71
-30
@@ -1,4 +1,5 @@
|
|||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
faCaretDown,
|
faCaretDown,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -17,12 +18,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 +37,6 @@ import {
|
|||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import {
|
import {
|
||||||
useCreateIdentityProjectAdditionalPrivilege,
|
useCreateIdentityProjectAdditionalPrivilege,
|
||||||
useGetIdentityProjectPrivilegeDetails,
|
useGetIdentityProjectPrivilegeDetails,
|
||||||
@@ -43,10 +44,10 @@ 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
|
||||||
@@ -88,7 +89,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,
|
||||||
@@ -98,7 +98,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
|||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isIdentityEditDisabled = permission.cannot(
|
const isIdentityEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
ProjectPermissionSub.Identity
|
subject(ProjectPermissionSub.Identity, { identityId })
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = useForm<TFormSchema>({
|
const form = useForm<TFormSchema>({
|
||||||
@@ -194,6 +194,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 +256,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((permissionSubject) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={`permission-create-${permissionSubject}`}
|
||||||
|
className="py-3"
|
||||||
|
onClick={() => onNewPolicy(permissionSubject as ProjectPermissionSub)}
|
||||||
|
>
|
||||||
|
{PROJECT_PERMISSION_OBJECT[permissionSubject as ProjectPermissionSub].title}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,17 +415,19 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
|||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-2 text-lg">Policies</div>
|
<div className="mb-2 text-lg">Policies</div>
|
||||||
{(isCreate || !isLoading) && <PermissionEmptyState />}
|
{(isCreate || !isLoading) && <PermissionEmptyState />}
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
||||||
<GeneralPermissionPolicies
|
(permissionSubject) => (
|
||||||
subject={subject}
|
<GeneralPermissionPolicies
|
||||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
subject={permissionSubject}
|
||||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
|
||||||
key={`project-permission-${subject}`}
|
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
|
||||||
isDisabled={isDisabled}
|
key={`project-permission-${permissionSubject}`}
|
||||||
>
|
isDisabled={isDisabled}
|
||||||
{renderConditionalComponents(subject, isDisabled)}
|
>
|
||||||
</GeneralPermissionPolicies>
|
{renderConditionalComponents(permissionSubject, isDisabled)}
|
||||||
))}
|
</GeneralPermissionPolicies>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+10
-3
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
+7
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import { subject } from "@casl/ability";
|
||||||
import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faFolder, faPencil, 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";
|
||||||
@@ -93,7 +94,9 @@ export const IdentityRoleDetailsSection = ({
|
|||||||
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
|
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Identity}
|
a={subject(ProjectPermissionSub.Identity, {
|
||||||
|
identityId: identityMembershipDetails.identity.id
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Edit Role(s)"
|
allowedLabel="Edit Role(s)"
|
||||||
>
|
>
|
||||||
@@ -175,7 +178,9 @@ export const IdentityRoleDetailsSection = ({
|
|||||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Member}
|
a={subject(ProjectPermissionSub.Identity, {
|
||||||
|
identityId: identityMembershipDetails.identity.id
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Remove Role"
|
allowedLabel="Remove Role"
|
||||||
>
|
>
|
||||||
|
|||||||
+57
-20
@@ -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
|
||||||
|
|||||||
-354
@@ -1,354 +0,0 @@
|
|||||||
/* eslint-disable no-nested-ternary */
|
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
|
||||||
import { faCaretDown, faClock, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { format, formatDistance } from "date-fns";
|
|
||||||
import ms from "ms";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Spinner,
|
|
||||||
Tag,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
useProjectPermission,
|
|
||||||
useSubscription,
|
|
||||||
useWorkspace
|
|
||||||
} from "@app/context";
|
|
||||||
import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api";
|
|
||||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
|
||||||
|
|
||||||
const roleFormSchema = z.object({
|
|
||||||
roles: z
|
|
||||||
.object({
|
|
||||||
slug: z.string(),
|
|
||||||
temporaryAccess: z.discriminatedUnion("isTemporary", [
|
|
||||||
z.object({
|
|
||||||
isTemporary: z.literal(true),
|
|
||||||
temporaryRange: z.string().min(1),
|
|
||||||
temporaryAccessStartTime: z.string().datetime(),
|
|
||||||
temporaryAccessEndTime: z.string().datetime().nullable().optional()
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
isTemporary: z.literal(false)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
})
|
|
||||||
.array()
|
|
||||||
});
|
|
||||||
type TRoleForm = z.infer<typeof roleFormSchema>;
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
identityProjectMember: IdentityMembership;
|
|
||||||
onOpenUpgradeModal: (title: string) => void;
|
|
||||||
};
|
|
||||||
export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal }: Props) => {
|
|
||||||
const { subscription } = useSubscription();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
|
||||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
|
||||||
const { permission } = useProjectPermission();
|
|
||||||
const isMemberEditDisabled = permission.cannot(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const roleForm = useForm<TRoleForm>({
|
|
||||||
resolver: zodResolver(roleFormSchema),
|
|
||||||
values: {
|
|
||||||
roles: identityProjectMember?.roles?.map(({ customRoleSlug, role, ...dto }) => ({
|
|
||||||
slug: customRoleSlug || role,
|
|
||||||
temporaryAccess: dto.isTemporary
|
|
||||||
? {
|
|
||||||
isTemporary: true,
|
|
||||||
temporaryRange: dto.temporaryRange,
|
|
||||||
temporaryAccessEndTime: dto.temporaryAccessEndTime,
|
|
||||||
temporaryAccessStartTime: dto.temporaryAccessStartTime
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
isTemporary: dto.isTemporary
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const selectedRoleList = useFieldArray({
|
|
||||||
name: "roles",
|
|
||||||
control: roleForm.control
|
|
||||||
});
|
|
||||||
|
|
||||||
const formRoleField = roleForm.watch("roles");
|
|
||||||
|
|
||||||
const updateMembershipRole = useUpdateIdentityWorkspaceRole();
|
|
||||||
|
|
||||||
const handleRoleUpdate = async (data: TRoleForm) => {
|
|
||||||
if (updateMembershipRole.isLoading) return;
|
|
||||||
|
|
||||||
const sanitizedRoles = data.roles.map((el) => {
|
|
||||||
const { isTemporary } = el.temporaryAccess;
|
|
||||||
if (!isTemporary) {
|
|
||||||
return { role: el.slug, isTemporary: false as const };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
role: el.slug,
|
|
||||||
isTemporary: true as const,
|
|
||||||
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
|
|
||||||
temporaryRange: el.temporaryAccess.temporaryRange,
|
|
||||||
temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasCustomRoleSelected = sanitizedRoles.some(
|
|
||||||
(el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasCustomRoleSelected && subscription && !subscription?.rbac) {
|
|
||||||
onOpenUpgradeModal(
|
|
||||||
"You can assign custom roles to members if you upgrade your Infisical plan."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateMembershipRole.mutateAsync({
|
|
||||||
workspaceId,
|
|
||||||
identityId: identityProjectMember.identity.id,
|
|
||||||
roles: sanitizedRoles
|
|
||||||
});
|
|
||||||
createNotification({ text: "Successfully updated roles", type: "success" });
|
|
||||||
roleForm.reset(undefined, { keepValues: true });
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({ text: "Failed to update role", type: "error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isRolesLoading)
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-center p-8">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="text-lg font-medium">Roles</div>
|
|
||||||
<p className="text-sm text-mineshaft-400">Select one of the pre-defined or custom roles.</p>
|
|
||||||
<div>
|
|
||||||
<form onSubmit={roleForm.handleSubmit(handleRoleUpdate)}>
|
|
||||||
<div className="mt-2 flex flex-col space-y-2">
|
|
||||||
{selectedRoleList.fields.map(({ id }, index) => {
|
|
||||||
const { temporaryAccess } = formRoleField[index];
|
|
||||||
const isTemporary = temporaryAccess?.isTemporary;
|
|
||||||
const isExpired =
|
|
||||||
temporaryAccess.isTemporary &&
|
|
||||||
new Date() > new Date(temporaryAccess.temporaryAccessEndTime || "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={id} className="flex items-center space-x-2">
|
|
||||||
<Controller
|
|
||||||
control={roleForm.control}
|
|
||||||
name={`roles.${index}.slug`}
|
|
||||||
render={({ field: { onChange, ...field } }) => (
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full bg-mineshaft-600"
|
|
||||||
>
|
|
||||||
{projectRoles?.map(({ name, slug, id: projectRoleId }) => (
|
|
||||||
<SelectItem value={slug} key={projectRoleId}>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger disabled={isMemberEditDisabled}>
|
|
||||||
<div>
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
temporaryAccess?.isTemporary
|
|
||||||
? isExpired
|
|
||||||
? "Timed Access Expired"
|
|
||||||
: `Until ${format(
|
|
||||||
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
|
||||||
"yyyy-MM-dd HH:mm:ss"
|
|
||||||
)}`
|
|
||||||
: "Non expiry access"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
|
||||||
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
className={twMerge(
|
|
||||||
"border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
|
|
||||||
isTemporary && "text-primary",
|
|
||||||
isExpired && "text-red-600"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{temporaryAccess?.isTemporary
|
|
||||||
? isExpired
|
|
||||||
? "Access Expired"
|
|
||||||
: formatDistance(
|
|
||||||
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
|
||||||
new Date()
|
|
||||||
)
|
|
||||||
: "Permanent"}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
arrowClassName="fill-gray-600"
|
|
||||||
side="right"
|
|
||||||
sideOffset={12}
|
|
||||||
hideCloseBtn
|
|
||||||
className="border border-gray-600 pt-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
|
||||||
Configure timed access
|
|
||||||
</div>
|
|
||||||
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
|
||||||
<Controller
|
|
||||||
control={roleForm.control}
|
|
||||||
defaultValue="1h"
|
|
||||||
name={`roles.${index}.temporaryAccess.temporaryRange`}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label={<TtlFormLabel label="Validity" />}
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
|
||||||
const temporaryRange = roleForm.getValues(
|
|
||||||
`roles.${index}.temporaryAccess.temporaryRange`
|
|
||||||
);
|
|
||||||
if (!temporaryRange) {
|
|
||||||
roleForm.setError(
|
|
||||||
`roles.${index}.temporaryAccess.temporaryRange`,
|
|
||||||
{ type: "required", message: "Required" },
|
|
||||||
{ shouldFocus: true }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
roleForm.clearErrors(`roles.${index}.temporaryAccess.temporaryRange`);
|
|
||||||
roleForm.setValue(
|
|
||||||
`roles.${index}.temporaryAccess`,
|
|
||||||
{
|
|
||||||
isTemporary: true,
|
|
||||||
temporaryAccessStartTime: new Date().toISOString(),
|
|
||||||
temporaryRange,
|
|
||||||
temporaryAccessEndTime: new Date(
|
|
||||||
new Date().getTime() + ms(temporaryRange)
|
|
||||||
).toISOString()
|
|
||||||
},
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{temporaryAccess.isTemporary ? "Restart" : "Grant"}
|
|
||||||
</Button>
|
|
||||||
{temporaryAccess.isTemporary && (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="outline_bg"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => {
|
|
||||||
roleForm.setValue(`roles.${index}.temporaryAccess`, {
|
|
||||||
isTemporary: false
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Revoke Access
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<IconButton
|
|
||||||
variant="outline_bg"
|
|
||||||
className="border border-mineshaft-500 bg-mineshaft-600 py-3 hover:border-red/70 hover:bg-red/20"
|
|
||||||
ariaLabel="delete-role"
|
|
||||||
isDisabled={isMemberEditDisabled || selectedRoleList.fields.length === 1}
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedRoleList.fields.length > 1) {
|
|
||||||
selectedRoleList.remove(index);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex justify-between space-x-2">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={ProjectPermissionSub.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() =>
|
|
||||||
selectedRoleList.append({
|
|
||||||
slug: ProjectMembershipRole.Member,
|
|
||||||
temporaryAccess: { isTemporary: false }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Add Role
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={twMerge(
|
|
||||||
"transition-all",
|
|
||||||
"cursor-default opacity-0",
|
|
||||||
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
|
||||||
)}
|
|
||||||
isDisabled={!roleForm.formState.isDirty}
|
|
||||||
isLoading={roleForm.formState.isSubmitting}
|
|
||||||
>
|
|
||||||
Save Roles
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { Alert, AlertDescription } from "@app/components/v2";
|
|
||||||
import { useWorkspace } from "@app/context";
|
|
||||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
|
||||||
|
|
||||||
import { IdentityRbacSection } from "./IdentityRbacSection";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
identityProjectMember: IdentityMembership;
|
|
||||||
onOpenUpgradeModal: (title: string) => void;
|
|
||||||
};
|
|
||||||
export const IdentityRoleForm = ({ identityProjectMember, onOpenUpgradeModal }: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<IdentityRbacSection
|
|
||||||
identityProjectMember={identityProjectMember}
|
|
||||||
onOpenUpgradeModal={onOpenUpgradeModal}
|
|
||||||
/>
|
|
||||||
<Alert
|
|
||||||
title="Additional privileges have been moved and now offer full permission customization."
|
|
||||||
className="mt-4 border-primary/50 bg-primary/10"
|
|
||||||
>
|
|
||||||
<AlertDescription>
|
|
||||||
<Link
|
|
||||||
href={`/project/${currentWorkspace?.id || ""}/identities/${
|
|
||||||
identityProjectMember?.identity?.id
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="cursor-pointer text-primary underline underline-offset-2">
|
|
||||||
Click here to access them now
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
export { IdentityRoleForm } from "./IdentityRoleForm";
|
|
||||||
+16
-9
@@ -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,17 +490,17 @@ 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: "Add", value: "create" },
|
||||||
{ label: "Modify", value: "edit" },
|
{ label: "Modify", value: "edit" },
|
||||||
{ 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" },
|
||||||
|
|||||||
+63
-21
@@ -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) => (
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
]);
|
]);
|
||||||
}}
|
}}
|
||||||
|
|||||||
+171
@@ -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}>In</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
-1
@@ -21,12 +21,13 @@ 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>
|
||||||
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
|
||||||
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
|
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
|
||||||
<SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>
|
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user