From 596378208e1d37a15c55a0440a010c8d72025d4b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 6 Mar 2025 23:39:39 +0800 Subject: [PATCH 01/27] misc: privilege management transition for org --- .../src/ee/services/group/group-service.ts | 51 +++++++++---- .../ee/services/permission/org-permission.ts | 60 ++++++++++++---- .../ee/services/permission/permission-fns.ts | 23 +++++- .../identity-aws-auth-service.ts | 21 ++++-- .../identity-azure-auth-service.ts | 22 ++++-- .../identity-gcp-auth-service.ts | 22 ++++-- .../identity-jwt-auth-service.ts | 22 ++++-- .../identity-kubernetes-auth-service.ts | 22 ++++-- .../identity-oidc-auth-service.ts | 21 ++++-- .../identity-token-auth-service.ts | 47 ++++++++---- .../identity-ua/identity-ua-service.ts | 72 ++++++++++++++----- .../src/services/identity/identity-service.ts | 53 ++++++-------- .../src/context/OrgPermissionContext/types.ts | 26 ++++++- 13 files changed, 327 insertions(+), 135 deletions(-) diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 7de3f8f92..fdb582bbf 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -3,7 +3,6 @@ import slugify from "@sindresorhus/slugify"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; @@ -14,7 +13,8 @@ import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TGroupDALFactory } from "./group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns"; @@ -74,7 +74,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); if (!plan.groups) @@ -87,7 +87,14 @@ export const groupServiceFactory = ({ actorOrgId ); const isCustomRole = Boolean(customRole); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + + const hasRequiredPriviledges = validatePrivilegeChangeOperation( + OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionSubjects.Groups, + permission, + rolePermission + ); + if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); @@ -135,7 +142,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); if (!plan.groups) @@ -156,7 +163,13 @@ export const groupServiceFactory = ({ ); const isCustomRole = Boolean(customOrgRole); - const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredNewRolePermission = validatePrivilegeChangeOperation( + OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionSubjects.Groups, + permission, + rolePermission + ); + if (!hasRequiredNewRolePermission) throw new ForbiddenRequestError({ message: "Failed to create a more privileged group" }); if (isCustomRole) customRole = customOrgRole; @@ -206,7 +219,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -233,7 +246,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findById(id); if (!group) { @@ -266,7 +279,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const group = await groupDAL.findOne({ orgId: actorOrgId, @@ -301,7 +314,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists const group = await groupDAL.findOne({ @@ -329,7 +342,13 @@ export const groupServiceFactory = ({ const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); // check if user has broader or equal to privileges than group - const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission); + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + OrgPermissionGroupActions.AddMembers, + OrgPermissionSubjects.Groups, + permission, + groupRolePermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" }); @@ -368,7 +387,7 @@ export const groupServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); // check if group with slug exists const group = await groupDAL.findOne({ @@ -396,7 +415,13 @@ export const groupServiceFactory = ({ const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); // check if user has broader or equal to privileges than group - const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission); + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + OrgPermissionGroupActions.RemoveMembers, + OrgPermissionSubjects.Groups, + permission, + groupRolePermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" }); diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index dbabcc2c1..9d0f8c380 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -40,6 +40,28 @@ export enum OrgPermissionGatewayActions { DeleteGateways = "delete-gateways" } +export enum OrgPermissionIdentityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges", + RevokeAuth = "revoke-auth", + CreateToken = "create-token", + GetToken = "get-token", + DeleteToken = "delete-token" +} + +export enum OrgPermissionGroupActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges", + AddMembers = "add-members", + RemoveMembers = "remove-members" +} + export enum OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -75,10 +97,10 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Ldap] - | [OrgPermissionActions, OrgPermissionSubjects.Groups] + | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] - | [OrgPermissionActions, OrgPermissionSubjects.Identity] + | [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] @@ -244,20 +266,28 @@ const buildAdminPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); - can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.ManagePrivileges, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.ManagePrivileges, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms); @@ -302,7 +332,7 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing); @@ -313,10 +343,10 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning); can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning); - can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 80a58db0a..b7913888a 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -1,7 +1,13 @@ +import { MongoAbility } from "@casl/ability"; + import { TOrganizations } from "@app/db/schemas"; +import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; +import { OrgPermissionSet } from "./org-permission"; +import { ProjectPermissionSet } from "./project-permission"; + function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { if (!actorAuthMethod) return false; @@ -43,4 +49,19 @@ const escapeHandlebarsMissingMetadata = (obj: Record) => { return new Proxy(obj, handler); }; -export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO }; +// This function serves as a transition layer between the old and new privilege management system +// the old privilege management system is based on the actor having more privileges than the managed permission +// the new privilege management system is based on the actor having the appropriate permission to perform the privilege change, +// regardless of the actor's privilege level. +const validatePrivilegeChangeOperation = ( + action: OrgPermissionSet[0] | ProjectPermissionSet[0], + subject: OrgPermissionSet[1] | ProjectPermissionSet[1], + actorPermission: MongoAbility, + managedPermission: MongoAbility +) => { + // first we ensure if the actor has the permission to manage the privilege + // if not, we check if the actor is indeed more privileged than the managed permission + return actorPermission.can(action, subject) || isAtLeastAsPrivileged(actorPermission, managedPermission); +}; + +export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO, validatePrivilegeChangeOperation }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index ff202f225..ad48223c0 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -5,9 +5,9 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -171,7 +171,7 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -250,7 +250,7 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -304,7 +304,7 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId }; }; @@ -329,7 +329,7 @@ export const identityAwsAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -339,7 +339,14 @@ export const identityAwsAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) throw new ForbiddenRequestError({ message: "Failed to revoke aws auth of identity with more privileged role" }); diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 01d013734..f011654a5 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -3,9 +3,9 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -143,7 +143,7 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -221,7 +221,7 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -277,7 +277,7 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId }; }; @@ -303,7 +303,7 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -312,7 +312,15 @@ export const identityAzureAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) throw new ForbiddenRequestError({ message: "Failed to revoke azure auth of identity with more privileged role" }); diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 5e404ca20..9aea92dba 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -3,9 +3,9 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -184,7 +184,7 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -264,7 +264,7 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -322,7 +322,7 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId }; }; @@ -349,7 +349,7 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -358,7 +358,15 @@ export const identityGcpAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to revoke gcp auth of identity with more privileged role" }); diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 6757b0b84..011a22465 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -5,9 +5,9 @@ import { JwksClient } from "jwks-rsa"; import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -264,7 +264,7 @@ export const identityJwtAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -367,7 +367,7 @@ export const identityJwtAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -456,7 +456,7 @@ export const identityJwtAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); @@ -498,7 +498,7 @@ export const identityJwtAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -508,7 +508,15 @@ export const identityJwtAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { + // revoke auth identity - org permission + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) { throw new ForbiddenRequestError({ message: "Failed to revoke JWT auth of identity with more privileged role" }); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index a5677894d..1be1eda3f 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -5,9 +5,9 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -249,7 +249,7 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -340,7 +340,7 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -434,7 +434,7 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -478,7 +478,7 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -487,7 +487,15 @@ export const identityKubernetesAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) throw new ForbiddenRequestError({ message: "Failed to revoke kubernetes auth of identity with more privileged role" }); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index ff7256a9c..bfdaba6d7 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -6,9 +6,9 @@ import { JwksClient } from "jwks-rsa"; import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -220,7 +220,7 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -310,7 +310,7 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -382,7 +382,7 @@ export const identityOidcAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); @@ -418,7 +418,7 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -428,7 +428,14 @@ export const identityOidcAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) { throw new ForbiddenRequestError({ message: "Failed to revoke OIDC auth of identity with more privileged role" }); diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index bf38c5fa1..74a47e1a0 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -3,9 +3,9 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; @@ -81,7 +81,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { @@ -154,7 +154,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { @@ -208,7 +208,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId }; }; @@ -235,7 +235,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -245,7 +245,14 @@ export const identityTokenAuthServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) { throw new ForbiddenRequestError({ message: "Failed to revoke Token Auth of identity with more privileged role" }); @@ -286,7 +293,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -295,7 +302,14 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + + const hasPriviledge = validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!hasPriviledge) throw new ForbiddenRequestError({ message: "Failed to create token for identity with more privileged role" @@ -363,7 +377,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const tokens = await identityAccessTokenDAL.find( { @@ -406,7 +420,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -415,7 +429,14 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); + + const hasPriviledge = validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!hasPriviledge) throw new ForbiddenRequestError({ message: "Failed to update token for identity with more privileged role" @@ -467,7 +488,7 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const [revokedToken] = await identityAccessTokenDAL.update( { diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index b9837265a..9a57d7eda 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -6,9 +6,9 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; @@ -172,7 +172,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { @@ -263,7 +263,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => { @@ -332,7 +332,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId }; }; @@ -358,7 +358,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -367,7 +367,15 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) throw new ForbiddenRequestError({ message: "Failed to revoke universal auth of identity with more privileged role" }); @@ -405,7 +413,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -414,8 +422,15 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); - if (!hasPriviledge) + + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) throw new ForbiddenRequestError({ message: "Failed to add identity to project with more privileged role" }); @@ -465,7 +480,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -475,9 +490,16 @@ export const identityUaServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) throw new ForbiddenRequestError({ - message: "Failed to add identity to project with more privileged role" + message: "Failed to get identity client secret with more privileged role" }); const identityUniversalAuth = await identityUaDAL.findOne({ @@ -515,7 +537,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -524,7 +546,15 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) throw new ForbiddenRequestError({ message: "Failed to read identity client secret of project with more privileged role" }); @@ -557,7 +587,7 @@ export const identityUaServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); const { permission: rolePermission } = await permissionService.getOrgPermission( ActorType.IDENTITY, @@ -567,10 +597,18 @@ export const identityUaServiceFactory = ({ actorOrgId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) + if ( + !validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.DeleteToken, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ) + ) { throw new ForbiddenRequestError({ message: "Failed to revoke identity client secret with more privileged role" }); + } const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, { isClientSecretRevoked: true diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index fffcbacc2..d17d6476a 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -2,13 +2,12 @@ import { ForbiddenError } from "@casl/ability"; import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; -import { ActorType } from "../auth/auth-type"; import { TIdentityDALFactory } from "./identity-dal"; import { TIdentityMetadataDALFactory } from "./identity-metadata-dal"; import { TIdentityOrgDALFactory } from "./identity-org-dal"; @@ -51,14 +50,19 @@ export const identityServiceFactory = ({ metadata }: TCreateIdentityDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( role, orgId ); const isCustomRole = Boolean(customRole); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredPriviledges = validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" }); @@ -120,18 +124,7 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); - - const { permission: identityRolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - id, - identityOrgMembership.orgId, - actorAuthMethod, - actorOrgId - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); let customRole: TOrgRoles | undefined; if (role) { @@ -141,7 +134,13 @@ export const identityServiceFactory = ({ ); const isCustomRole = Boolean(customOrgRole); - const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredNewRolePermission = validatePrivilegeChangeOperation( + OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionSubjects.Identity, + permission, + rolePermission + ); + if (!hasRequiredNewRolePermission) throw new ForbiddenRequestError({ message: "Failed to create a more privileged identity" }); if (isCustomRole) customRole = customOrgRole; @@ -193,7 +192,7 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); return identity; }; @@ -208,17 +207,7 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity); - const { permission: identityRolePermission } = await permissionService.getOrgPermission( - ActorType.IDENTITY, - id, - identityOrgMembership.orgId, - actorAuthMethod, - actorOrgId - ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); const deletedIdentity = await identityDAL.deleteById(id); @@ -240,7 +229,7 @@ export const identityServiceFactory = ({ search }: TListOrgIdentitiesByOrgIdDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityOrgMembershipDAL.find({ [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, @@ -276,7 +265,7 @@ export const identityServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); const identityMemberships = await identityProjectDAL.findByIdentityId(identityId); return identityMemberships; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 2a31ed6a8..854230eb0 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -54,6 +54,27 @@ export enum OrgPermissionKmipActions { Setup = "setup" } +export enum OrgPermissionIdentityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges", + RevokeAuth = "revoke-auth", + CreateToken = "create-token", + GetToken = "get-token" +} + +export enum OrgPermissionGroupActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges", + AddMembers = "add-members", + RemoveMembers = "remove-members" +} + export type AppConnectionSubjectFields = { connectionId: string; }; @@ -68,17 +89,18 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Ldap] - | [OrgPermissionActions, OrgPermissionSubjects.Groups] + | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.Billing] - | [OrgPermissionActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections] + | [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity] | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]; + // TODO(scott): add back once org UI refactored // | [ // OrgPermissionAppConnectionActions, From 5ed164de24fd543fbc0d6bf493010da92b22c01d Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 7 Mar 2025 01:33:29 +0800 Subject: [PATCH 02/27] misc: project permission transition --- ...project-additional-privilege-v2-service.ts | 41 ++++++--- ...ty-project-additional-privilege-service.ts | 48 +++++++--- .../services/permission/project-permission.ts | 90 +++++++++++++++---- ...oject-user-additional-privilege-service.ts | 36 +++++--- .../group-project/group-project-service.ts | 28 ++++-- .../identity-project-service.ts | 44 ++++----- .../project-membership-service.ts | 25 +++--- 7 files changed, 217 insertions(+), 95 deletions(-) diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index eb9c66c1c..6fbc27702 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -3,15 +3,15 @@ import { packRules } from "@casl/ability/extra"; import ms from "ms"; import { ActionProjectType, TableName } from "@app/db/schemas"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; import { IdentityProjectAdditionalPrivilegeTemporaryMode, @@ -64,7 +64,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ @@ -79,8 +79,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ @@ -146,7 +151,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ @@ -161,8 +166,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); if (data?.slug) { @@ -228,7 +238,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); const { permission: identityRolePermission } = await permissionService.getProjectPermission({ @@ -239,7 +249,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + const hasRequiredPriviledges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + identityRolePermission + ); if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -275,7 +290,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionIdentityActions.Read, subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); @@ -310,7 +325,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionIdentityActions.Read, subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); @@ -346,7 +361,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionIdentityActions.Read, subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index d74f9c504..3b9021b30 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -3,15 +3,19 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import ms from "ms"; import { ActionProjectType } from "@app/db/schemas"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionIdentityActions, + ProjectPermissionSet, + ProjectPermissionSub +} from "../permission/project-permission"; import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; import { IdentityProjectAdditionalPrivilegeTemporaryMode, @@ -71,8 +75,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); + ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); @@ -88,8 +93,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ @@ -156,7 +166,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); @@ -172,8 +182,13 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetIdentityPermission); - if (!hasRequiredPriviledges) + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + targetIdentityPermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ @@ -256,7 +271,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); @@ -268,9 +283,14 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); - if (!hasRequiredPriviledges) - throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); + + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + identityRolePermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ slug, @@ -315,7 +335,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionIdentityActions.Read, subject(ProjectPermissionSub.Identity, { identityId }) ); @@ -359,7 +379,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionIdentityActions.Read, subject(ProjectPermissionSub.Identity, { identityId }) ); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 4389c3866..1cce302a4 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -34,6 +34,30 @@ export enum ProjectPermissionDynamicSecretActions { Lease = "lease" } +export enum ProjectPermissionIdentityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges" +} + +export enum ProjectPermissionMemberActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges" +} + +export enum ProjectPermissionGroupActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges" +} + export enum ProjectPermissionSecretSyncActions { Read = "read", Create = "create", @@ -141,8 +165,8 @@ export type ProjectPermissionSet = ] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] - | [ProjectPermissionActions, ProjectPermissionSub.Member] - | [ProjectPermissionActions, ProjectPermissionSub.Groups] + | [ProjectPermissionMemberActions, ProjectPermissionSub.Member] + | [ProjectPermissionGroupActions, ProjectPermissionSub.Groups] | [ProjectPermissionActions, ProjectPermissionSub.Integrations] | [ProjectPermissionActions, ProjectPermissionSub.Webhooks] | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] @@ -153,7 +177,7 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ - ProjectPermissionActions, + ProjectPermissionIdentityActions, ProjectPermissionSub.Identity | (ForcedSubject & IdentityManagementSubjectFields) ] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] @@ -281,13 +305,13 @@ const GeneralPermissionSchema = [ }), z.object({ subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionMemberActions).describe( "Describe what action an entity can take." ) }), z.object({ subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionGroupActions).describe( "Describe what action an entity can take." ) }), @@ -449,7 +473,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionIdentityActions).describe( "Describe what action an entity can take." ) }), @@ -522,12 +546,9 @@ const buildAdminPermissionRules = () => { ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretApproval, ProjectPermissionSub.SecretRotation, - ProjectPermissionSub.Member, - ProjectPermissionSub.Groups, ProjectPermissionSub.Role, ProjectPermissionSub.Integrations, ProjectPermissionSub.Webhooks, - ProjectPermissionSub.Identity, ProjectPermissionSub.ServiceTokens, ProjectPermissionSub.Settings, ProjectPermissionSub.Environments, @@ -554,6 +575,39 @@ const buildAdminPermissionRules = () => { ); }); + can( + [ + ProjectPermissionMemberActions.Create, + ProjectPermissionMemberActions.Edit, + ProjectPermissionMemberActions.Delete, + ProjectPermissionMemberActions.Read, + ProjectPermissionMemberActions.ManagePrivileges + ], + ProjectPermissionSub.Member + ); + + can( + [ + ProjectPermissionGroupActions.Create, + ProjectPermissionGroupActions.Edit, + ProjectPermissionGroupActions.Delete, + ProjectPermissionGroupActions.Read, + ProjectPermissionGroupActions.ManagePrivileges + ], + ProjectPermissionSub.Groups + ); + + can( + [ + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Delete, + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.ManagePrivileges + ], + ProjectPermissionSub.Identity + ); + can( [ ProjectPermissionDynamicSecretActions.ReadRootCredential, @@ -654,9 +708,9 @@ const buildMemberPermissionRules = () => { can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback); - can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member); + can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member); - can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups); + can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups); can( [ @@ -680,10 +734,10 @@ const buildMemberPermissionRules = () => { can( [ - ProjectPermissionActions.Read, - ProjectPermissionActions.Edit, - ProjectPermissionActions.Create, - ProjectPermissionActions.Delete + ProjectPermissionIdentityActions.Read, + ProjectPermissionIdentityActions.Edit, + ProjectPermissionIdentityActions.Create, + ProjectPermissionIdentityActions.Delete ], ProjectPermissionSub.Identity ); @@ -795,12 +849,12 @@ const buildViewerPermissionRules = () => { can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); + can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); - can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity); can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens); can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings); can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments); diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 6f87663b2..f2b37d606 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -3,14 +3,18 @@ import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import ms from "ms"; import { ActionProjectType, TableName } from "@app/db/schemas"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission"; +import { + ProjectPermissionMemberActions, + ProjectPermissionSet, + ProjectPermissionSub +} from "../permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; import { ProjectUserAdditionalPrivilegeTemporaryMode, @@ -63,7 +67,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const { permission: targetUserPermission } = await permissionService.getProjectPermission({ actor: ActorType.USER, actorId: projectMembership.userId, @@ -76,7 +80,14 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); + + const hasRequiredPriviledges = validatePrivilegeChangeOperation( + ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionSub.Member, + permission, + targetUserPermission + ); + if (!hasRequiredPriviledges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); @@ -150,7 +161,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const { permission: targetUserPermission } = await permissionService.getProjectPermission({ actor: ActorType.USER, actorId: projectMembership.userId, @@ -163,8 +174,13 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ // we need to validate that the privilege given is not higher than the assigning users permission // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, targetUserPermission); - if (!hasRequiredPriviledges) + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionSub.Member, + permission, + targetUserPermission + ); + if (!hasRequiredPrivileges) throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); if (dto?.slug) { @@ -236,7 +252,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); return { @@ -273,7 +289,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); return { ...userPrivilege, @@ -300,7 +316,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const userPrivileges = await projectUserAdditionalPrivilegeDAL.find( { diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 067ff17b0..000380a8e 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -2,9 +2,9 @@ import { ForbiddenError } from "@casl/ability"; import ms from "ms"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -78,7 +78,7 @@ export const groupProjectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups); let group: TGroups | null = null; if (isUuidV4(groupIdOrName)) { @@ -102,7 +102,12 @@ export const groupProjectServiceFactory = ({ project.id ); - const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionSub.Groups, + permission, + rolePermission + ); if (!hasRequiredPrivileges) { throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); @@ -254,7 +259,7 @@ export const groupProjectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups); const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId }); if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` }); @@ -268,7 +273,12 @@ export const groupProjectServiceFactory = ({ project.id ); - const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredPrivileges = validatePrivilegeChangeOperation( + ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionSub.Groups, + permission, + rolePermission + ); if (!hasRequiredPrivileges) { throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" }); @@ -357,7 +367,7 @@ export const groupProjectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups); const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => { const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx); @@ -402,7 +412,7 @@ export const groupProjectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); const groupMemberships = await groupProjectDAL.findByProjectId(project.id); return groupMemberships; @@ -430,7 +440,7 @@ export const groupProjectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, { groupId diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index 36b9b0562..5f14e3ace 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -2,13 +2,12 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; -import { ActorType } from "../auth/auth-type"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TProjectDALFactory } from "../project/project-dal"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; @@ -63,7 +62,7 @@ export const identityProjectServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionIdentityActions.Create, subject(ProjectPermissionSub.Identity, { identityId }) @@ -91,7 +90,12 @@ export const identityProjectServiceFactory = ({ projectId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredPriviledges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + rolePermission + ); if (!hasRequiredPriviledges) { throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" }); @@ -169,7 +173,7 @@ export const identityProjectServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, + ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); @@ -185,7 +189,13 @@ export const identityProjectServiceFactory = ({ projectId ); - if (!isAtLeastAsPrivileged(permission, rolePermission)) { + const hasRequiredPriviledges = validatePrivilegeChangeOperation( + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity, + permission, + rolePermission + ); + if (!hasRequiredPriviledges) { throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" }); } } @@ -265,21 +275,10 @@ export const identityProjectServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, + ProjectPermissionIdentityActions.Delete, subject(ProjectPermissionSub.Identity, { identityId }) ); - const { permission: identityRolePermission } = await permissionService.getProjectPermission({ - actor: ActorType.IDENTITY, - actorId: identityId, - projectId: identityProjectMembership.projectId, - actorAuthMethod, - actorOrgId, - actionProjectType: ActionProjectType.Any - }); - if (!isAtLeastAsPrivileged(permission, identityRolePermission)) - throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" }); - const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId }); return deletedIdentity; }; @@ -304,7 +303,10 @@ export const identityProjectServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionIdentityActions.Read, + ProjectPermissionSub.Identity + ); const identityMemberships = await identityProjectDAL.findByProjectId(projectId, { limit, @@ -337,7 +339,7 @@ export const identityProjectServiceFactory = ({ }); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, + ProjectPermissionIdentityActions.Read, subject(ProjectPermissionSub.Identity, { identityId }) ); diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index fd1382dcf..18e42dc03 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -4,10 +4,10 @@ import ms from "ms"; import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; -import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; @@ -86,7 +86,7 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId); @@ -130,7 +130,7 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username }); if (!membership) throw new NotFoundError({ message: `Project membership not found for user '${username}'` }); @@ -153,7 +153,7 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id }); if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` }); @@ -180,7 +180,7 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member); const orgMembers = await orgDAL.findMembership({ [`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId, $in: { @@ -261,7 +261,7 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId); if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) { @@ -274,7 +274,12 @@ export const projectMembershipServiceFactory = ({ projectId ); - const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission); + const hasRequiredPriviledges = validatePrivilegeChangeOperation( + ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionSub.Member, + permission, + rolePermission + ); if (!hasRequiredPriviledges) { throw new ForbiddenRequestError({ @@ -361,7 +366,7 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); const member = await userDAL.findUserByProjectMembershipId(membershipId); @@ -397,7 +402,7 @@ export const projectMembershipServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member); const project = await projectDAL.findById(projectId); From a3fb7c9f00d08a167a96816b08d0264da0805174 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 7 Mar 2025 02:39:03 +0800 Subject: [PATCH 03/27] misc: add org-level UI changes for role --- .../src/context/OrgPermissionContext/types.ts | 3 +- .../components/OrgRoleModifySection.utils.ts | 32 ++- .../OrgPermissionGroupRow.tsx | 207 +++++++++++++++++ .../OrgPermissionIdentityRow.tsx | 217 ++++++++++++++++++ .../RolePermissionsSection.tsx | 20 +- 5 files changed, 468 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGroupRow.tsx create mode 100644 frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionIdentityRow.tsx diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 854230eb0..c2873e6f9 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -62,7 +62,8 @@ export enum OrgPermissionIdentityActions { ManagePrivileges = "manage-privileges", RevokeAuth = "revoke-auth", CreateToken = "create-token", - GetToken = "get-token" + GetToken = "get-token", + DeleteToken = "delete-token" } export enum OrgPermissionGroupActions { diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index 42dd31417..56d77ae60 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -5,6 +5,8 @@ import { OrgPermissionSubjects } from "@app/context"; import { OrgGatewayPermissionActions, OrgPermissionAppConnectionActions, + OrgPermissionGroupActions, + OrgPermissionIdentityActions, OrgPermissionKmipActions } from "@app/context/OrgPermissionContext/types"; import { TPermission } from "@app/hooks/api/roles/types"; @@ -35,6 +37,32 @@ const kmipPermissionSchema = z }) .optional(); +const identityPermissionSchema = z + .object({ + [OrgPermissionIdentityActions.Read]: z.boolean().optional(), + [OrgPermissionIdentityActions.Edit]: z.boolean().optional(), + [OrgPermissionIdentityActions.Delete]: z.boolean().optional(), + [OrgPermissionIdentityActions.Create]: z.boolean().optional(), + [OrgPermissionIdentityActions.ManagePrivileges]: z.boolean().optional(), + [OrgPermissionIdentityActions.RevokeAuth]: z.boolean().optional(), + [OrgPermissionIdentityActions.CreateToken]: z.boolean().optional(), + [OrgPermissionIdentityActions.GetToken]: z.boolean().optional(), + [OrgPermissionIdentityActions.DeleteToken]: z.boolean().optional() + }) + .optional(); + +const groupPermissionSchema = z + .object({ + [OrgPermissionGroupActions.Read]: z.boolean().optional(), + [OrgPermissionGroupActions.Create]: z.boolean().optional(), + [OrgPermissionGroupActions.Edit]: z.boolean().optional(), + [OrgPermissionGroupActions.Delete]: z.boolean().optional(), + [OrgPermissionGroupActions.ManagePrivileges]: z.boolean().optional(), + [OrgPermissionGroupActions.AddMembers]: z.boolean().optional(), + [OrgPermissionGroupActions.RemoveMembers]: z.boolean().optional() + }) + .optional(); + const orgGatewayPermissionSchema = z .object({ [OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(), @@ -67,7 +95,7 @@ export const formSchema = z.object({ "audit-logs": generalPermissionSchema, member: generalPermissionSchema, - groups: generalPermissionSchema, + groups: groupPermissionSchema, role: generalPermissionSchema, settings: generalPermissionSchema, "service-account": generalPermissionSchema, @@ -77,7 +105,7 @@ export const formSchema = z.object({ scim: generalPermissionSchema, ldap: generalPermissionSchema, billing: generalPermissionSchema, - identity: generalPermissionSchema, + identity: identityPermissionSchema, "organization-admin-console": adminConsolePermissionSchmea, [OrgPermissionSubjects.Kms]: generalPermissionSchema, [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema, diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGroupRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGroupRow.tsx new file mode 100644 index 000000000..eafdd3583 --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGroupRow.tsx @@ -0,0 +1,207 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { OrgPermissionGroupActions } from "@app/context/OrgPermissionContext/types"; +import { useToggle } from "@app/hooks"; + +import { TFormSchema } from "../OrgRoleModifySection.utils"; + +const PERMISSION_ACTIONS = [ + { action: OrgPermissionGroupActions.Read, label: "Read Groups" }, + { action: OrgPermissionGroupActions.Create, label: "Create Groups" }, + { action: OrgPermissionGroupActions.Edit, label: "Edit Groups" }, + { action: OrgPermissionGroupActions.Delete, label: "Delete Groups" }, + { action: OrgPermissionGroupActions.ManagePrivileges, label: "Manage Privileges" }, + { action: OrgPermissionGroupActions.AddMembers, label: "Add Members" }, + { action: OrgPermissionGroupActions.RemoveMembers, label: "Remove Members" } +] as const; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + ReadOnly = "read-only", + FullAccess = "full-access", + Custom = "custom" +} + +export const OrgPermissionGroupRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.groups" + }); + + const selectedPermissionCategory = useMemo(() => { + const actions = Object.keys(rule || {}) as Array; + const totalActions = PERMISSION_ACTIONS.length; + const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); + + if (isCustom) return Permission.Custom; + if (score === 0) return Permission.NoAccess; + if (score === totalActions) return Permission.FullAccess; + if (score === 1 && rule?.read) return Permission.ReadOnly; + + return Permission.Custom; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + switch (val) { + case Permission.NoAccess: + setValue( + "permissions.groups", + { + [OrgPermissionGroupActions.Read]: false, + [OrgPermissionGroupActions.Create]: false, + [OrgPermissionGroupActions.Edit]: false, + [OrgPermissionGroupActions.Delete]: false, + [OrgPermissionGroupActions.ManagePrivileges]: false, + [OrgPermissionGroupActions.AddMembers]: false, + [OrgPermissionGroupActions.RemoveMembers]: false + }, + { shouldDirty: true } + ); + break; + case Permission.FullAccess: + setValue( + "permissions.groups", + { + [OrgPermissionGroupActions.Read]: true, + [OrgPermissionGroupActions.Create]: true, + [OrgPermissionGroupActions.Edit]: true, + [OrgPermissionGroupActions.Delete]: true, + [OrgPermissionGroupActions.ManagePrivileges]: true, + [OrgPermissionGroupActions.AddMembers]: true, + [OrgPermissionGroupActions.RemoveMembers]: true + }, + { shouldDirty: true } + ); + break; + case Permission.ReadOnly: + setValue( + "permissions.groups", + { + [OrgPermissionGroupActions.Read]: true, + [OrgPermissionGroupActions.Edit]: false, + [OrgPermissionGroupActions.Create]: false, + [OrgPermissionGroupActions.Delete]: false, + [OrgPermissionGroupActions.ManagePrivileges]: false, + [OrgPermissionGroupActions.AddMembers]: false, + [OrgPermissionGroupActions.RemoveMembers]: false + }, + { shouldDirty: true } + ); + break; + default: + setValue( + "permissions.groups", + { + [OrgPermissionGroupActions.Read]: false, + [OrgPermissionGroupActions.Edit]: false, + [OrgPermissionGroupActions.Create]: false, + [OrgPermissionGroupActions.Delete]: false, + [OrgPermissionGroupActions.ManagePrivileges]: false, + [OrgPermissionGroupActions.AddMembers]: false, + [OrgPermissionGroupActions.RemoveMembers]: false + }, + { shouldDirty: true } + ); + break; + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Group Management + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.groups.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionIdentityRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionIdentityRow.tsx new file mode 100644 index 000000000..e1700c17a --- /dev/null +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionIdentityRow.tsx @@ -0,0 +1,217 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { OrgPermissionIdentityActions } from "@app/context/OrgPermissionContext/types"; +import { useToggle } from "@app/hooks"; + +import { TFormSchema } from "../OrgRoleModifySection.utils"; + +const PERMISSION_ACTIONS = [ + { action: OrgPermissionIdentityActions.Read, label: "Read Identities" }, + { action: OrgPermissionIdentityActions.Create, label: "Create Identities" }, + { action: OrgPermissionIdentityActions.Edit, label: "Edit Identities" }, + { action: OrgPermissionIdentityActions.Delete, label: "Delete Identities" }, + { action: OrgPermissionIdentityActions.ManagePrivileges, label: "Manage Privileges" }, + { action: OrgPermissionIdentityActions.RevokeAuth, label: "Revoke Auth" }, + { action: OrgPermissionIdentityActions.CreateToken, label: "Create Token" }, + { action: OrgPermissionIdentityActions.GetToken, label: "Get Token" }, + { action: OrgPermissionIdentityActions.DeleteToken, label: "Delete Token" } +] as const; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + ReadOnly = "read-only", + FullAccess = "full-access", + Custom = "custom" +} + +export const OrgPermissionIdentityRow = ({ isEditable, control, setValue }: Props) => { + const [isRowExpanded, setIsRowExpanded] = useToggle(); + const [isCustom, setIsCustom] = useToggle(); + + const rule = useWatch({ + control, + name: "permissions.identity" + }); + + const selectedPermissionCategory = useMemo(() => { + const actions = Object.keys(rule || {}) as Array; + const totalActions = PERMISSION_ACTIONS.length; + const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); + + if (isCustom) return Permission.Custom; + if (score === 0) return Permission.NoAccess; + if (score === totalActions) return Permission.FullAccess; + if (score === 1 && rule?.read) return Permission.ReadOnly; + + return Permission.Custom; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + useEffect(() => { + const isRowCustom = selectedPermissionCategory === Permission.Custom; + if (isRowCustom) { + setIsRowExpanded.on(); + } + }, []); + + const handlePermissionChange = (val: Permission) => { + if (val === Permission.Custom) { + setIsRowExpanded.on(); + setIsCustom.on(); + return; + } + setIsCustom.off(); + + switch (val) { + case Permission.NoAccess: + setValue( + "permissions.identity", + { + [OrgPermissionIdentityActions.Read]: false, + [OrgPermissionIdentityActions.Edit]: false, + [OrgPermissionIdentityActions.Create]: false, + [OrgPermissionIdentityActions.Delete]: false, + [OrgPermissionIdentityActions.ManagePrivileges]: false, + [OrgPermissionIdentityActions.RevokeAuth]: false, + [OrgPermissionIdentityActions.CreateToken]: false, + [OrgPermissionIdentityActions.GetToken]: false, + [OrgPermissionIdentityActions.DeleteToken]: false + }, + { shouldDirty: true } + ); + break; + case Permission.FullAccess: + setValue( + "permissions.identity", + { + [OrgPermissionIdentityActions.Read]: true, + [OrgPermissionIdentityActions.Edit]: true, + [OrgPermissionIdentityActions.Create]: true, + [OrgPermissionIdentityActions.Delete]: true, + [OrgPermissionIdentityActions.ManagePrivileges]: true, + [OrgPermissionIdentityActions.RevokeAuth]: true, + [OrgPermissionIdentityActions.CreateToken]: true, + [OrgPermissionIdentityActions.GetToken]: true, + [OrgPermissionIdentityActions.DeleteToken]: true + }, + { shouldDirty: true } + ); + break; + case Permission.ReadOnly: + setValue( + "permissions.identity", + { + [OrgPermissionIdentityActions.Read]: true, + [OrgPermissionIdentityActions.Edit]: false, + [OrgPermissionIdentityActions.Create]: false, + [OrgPermissionIdentityActions.Delete]: false, + [OrgPermissionIdentityActions.ManagePrivileges]: false, + [OrgPermissionIdentityActions.RevokeAuth]: false, + [OrgPermissionIdentityActions.CreateToken]: false, + [OrgPermissionIdentityActions.GetToken]: false, + [OrgPermissionIdentityActions.DeleteToken]: false + }, + { shouldDirty: true } + ); + break; + default: + setValue( + "permissions.identity", + { + [OrgPermissionIdentityActions.Read]: false, + [OrgPermissionIdentityActions.Edit]: false, + [OrgPermissionIdentityActions.Create]: false, + [OrgPermissionIdentityActions.Delete]: false, + [OrgPermissionIdentityActions.ManagePrivileges]: false, + [OrgPermissionIdentityActions.RevokeAuth]: false, + [OrgPermissionIdentityActions.CreateToken]: false, + [OrgPermissionIdentityActions.GetToken]: false, + [OrgPermissionIdentityActions.DeleteToken]: false + }, + { shouldDirty: true } + ); + break; + } + }; + + return ( + <> + setIsRowExpanded.toggle()} + > + + + + Machine Identity Management + + + + + {isRowExpanded && ( + + +
+ {PERMISSION_ACTIONS.map(({ action, label }) => { + return ( + ( + { + if (!isEditable) { + createNotification({ + type: "error", + text: "Failed to update default role" + }); + return; + } + field.onChange(e); + }} + id={`permissions.identity.${action}`} + > + {label} + + )} + /> + ); + })} +
+ + + )} + + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index de9fbde80..1816f4292 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -15,6 +15,8 @@ import { } from "../OrgRoleModifySection.utils"; import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow"; +import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow"; +import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow"; import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; @@ -24,14 +26,6 @@ const SIMPLE_PERMISSION_OPTIONS = [ title: "User Management", formName: "member" }, - { - title: "Group Management", - formName: "groups" - }, - { - title: "Machine Identity Management", - formName: "identity" - }, { title: "Usage & Billing", formName: "billing" @@ -167,6 +161,16 @@ export const RolePermissionsSection = ({ roleId }: Props) => { /> ); })} + + Date: Sat, 8 Mar 2025 04:10:06 +0800 Subject: [PATCH 04/27] misc: updated project rbac fe --- .../services/permission/project-permission.ts | 4 +- .../context/ProjectPermissionContext/types.ts | 24 ++++ .../ProjectRoleModifySection.utils.tsx | 122 +++++++++++++++--- 3 files changed, 130 insertions(+), 20 deletions(-) diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 1cce302a4..c5c731dc7 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -473,7 +473,7 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ }), z.object({ subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."), - action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionIdentityActions).describe( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( "Describe what action an entity can take." ) }), @@ -524,7 +524,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [ 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( + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionIdentityActions).describe( "Describe what action an entity can take." ), conditions: IdentityManagementConditionSchema.describe( diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d368a949f..6d274734e 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -42,6 +42,30 @@ export enum ProjectPermissionSecretSyncActions { RemoveSecrets = "remove-secrets" } +export enum ProjectPermissionIdentityActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges" +} + +export enum ProjectPermissionMemberActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges" +} + +export enum ProjectPermissionGroupActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + ManagePrivileges = "manage-privileges" +} + export enum PermissionConditionOperators { $IN = "$in", $ALL = "$all", diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 7dcb118fd..c1dfb51b6 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -8,7 +8,10 @@ import { import { PermissionConditionOperators, ProjectPermissionDynamicSecretActions, + ProjectPermissionGroupActions, + ProjectPermissionIdentityActions, ProjectPermissionKmipActions, + ProjectPermissionMemberActions, ProjectPermissionSecretSyncActions, TPermissionCondition, TPermissionConditionOperators @@ -57,6 +60,30 @@ const KmipPolicyActionSchema = z.object({ [ProjectPermissionKmipActions.GenerateClientCertificates]: z.boolean().optional() }); +const MemberPolicyActionSchema = z.object({ + [ProjectPermissionMemberActions.Read]: z.boolean().optional(), + [ProjectPermissionMemberActions.Create]: z.boolean().optional(), + [ProjectPermissionMemberActions.Edit]: z.boolean().optional(), + [ProjectPermissionMemberActions.Delete]: z.boolean().optional(), + [ProjectPermissionMemberActions.ManagePrivileges]: z.boolean().optional() +}); + +const IdentityPolicyActionSchema = z.object({ + [ProjectPermissionIdentityActions.Read]: z.boolean().optional(), + [ProjectPermissionIdentityActions.Create]: z.boolean().optional(), + [ProjectPermissionIdentityActions.Edit]: z.boolean().optional(), + [ProjectPermissionIdentityActions.Delete]: z.boolean().optional(), + [ProjectPermissionIdentityActions.ManagePrivileges]: z.boolean().optional() +}); + +const GroupPolicyActionSchema = z.object({ + [ProjectPermissionGroupActions.Read]: z.boolean().optional(), + [ProjectPermissionGroupActions.Create]: z.boolean().optional(), + [ProjectPermissionGroupActions.Edit]: z.boolean().optional(), + [ProjectPermissionGroupActions.Delete]: z.boolean().optional(), + [ProjectPermissionGroupActions.ManagePrivileges]: z.boolean().optional() +}); + const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -138,14 +165,14 @@ export const projectRoleFormSchema = z.object({ }) .array() .default([]), - [ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.extend({ + [ProjectPermissionSub.Identity]: IdentityPolicyActionSchema.extend({ inverted: z.boolean().optional(), conditions: ConditionSchema }) .array() .default([]), - [ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]), - [ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]), + [ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]), + [ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]), [ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Integrations]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Webhooks]: GeneralPolicyActionSchema.array().default([]), @@ -234,9 +261,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { ProjectPermissionSub.DynamicSecrets, ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretImports, - ProjectPermissionSub.Member, - ProjectPermissionSub.Groups, - ProjectPermissionSub.Identity, ProjectPermissionSub.Role, ProjectPermissionSub.Integrations, ProjectPermissionSub.Webhooks, @@ -391,6 +415,65 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { return; } + if (subject === ProjectPermissionSub.Member) { + const canRead = action.includes(ProjectPermissionMemberActions.Read); + const canCreate = action.includes(ProjectPermissionMemberActions.Create); + const canEdit = action.includes(ProjectPermissionMemberActions.Edit); + const canDelete = action.includes(ProjectPermissionMemberActions.Delete); + const canManagePrivileges = action.includes(ProjectPermissionMemberActions.ManagePrivileges); + + if (!formVal[subject]) formVal[subject] = [{}]; + + // from above statement we are sure it won't be undefined + if (canRead) formVal[subject]![0][ProjectPermissionMemberActions.Read] = true; + if (canCreate) formVal[subject]![0][ProjectPermissionMemberActions.Create] = true; + if (canEdit) formVal[subject]![0][ProjectPermissionMemberActions.Edit] = true; + if (canDelete) formVal[subject]![0][ProjectPermissionMemberActions.Delete] = true; + if (canManagePrivileges) + formVal[subject]![0][ProjectPermissionMemberActions.ManagePrivileges] = true; + return; + } + + if (subject === ProjectPermissionSub.Identity) { + const canRead = action.includes(ProjectPermissionIdentityActions.Read); + const canCreate = action.includes(ProjectPermissionIdentityActions.Create); + const canEdit = action.includes(ProjectPermissionIdentityActions.Edit); + const canDelete = action.includes(ProjectPermissionIdentityActions.Delete); + const canManagePrivileges = action.includes( + ProjectPermissionIdentityActions.ManagePrivileges + ); + + if (!formVal[subject]) formVal[subject] = [{ conditions: [] }]; + + // from above statement we are sure it won't be undefined + if (canRead) formVal[subject]![0][ProjectPermissionIdentityActions.Read] = true; + if (canCreate) formVal[subject]![0][ProjectPermissionIdentityActions.Create] = true; + if (canEdit) formVal[subject]![0][ProjectPermissionIdentityActions.Edit] = true; + if (canDelete) formVal[subject]![0][ProjectPermissionIdentityActions.Delete] = true; + if (canManagePrivileges) + formVal[subject]![0][ProjectPermissionIdentityActions.ManagePrivileges] = true; + return; + } + + if (subject === ProjectPermissionSub.Groups) { + const canRead = action.includes(ProjectPermissionGroupActions.Read); + const canCreate = action.includes(ProjectPermissionGroupActions.Create); + const canEdit = action.includes(ProjectPermissionGroupActions.Edit); + const canDelete = action.includes(ProjectPermissionGroupActions.Delete); + const canManagePrivileges = action.includes(ProjectPermissionGroupActions.ManagePrivileges); + + if (!formVal[subject]) formVal[subject] = [{}]; + + // from above statement we are sure it won't be undefined + if (canRead) formVal[subject]![0][ProjectPermissionGroupActions.Read] = true; + if (canCreate) formVal[subject]![0][ProjectPermissionGroupActions.Create] = true; + if (canEdit) formVal[subject]![0][ProjectPermissionGroupActions.Edit] = true; + if (canDelete) formVal[subject]![0][ProjectPermissionGroupActions.Delete] = true; + if (canManagePrivileges) + formVal[subject]![0][ProjectPermissionGroupActions.ManagePrivileges] = true; + return; + } + if (subject === ProjectPermissionSub.SecretSyncs) { const canRead = action.includes(ProjectPermissionSecretSyncActions.Read); const canEdit = action.includes(ProjectPermissionSecretSyncActions.Edit); @@ -571,28 +654,31 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { [ProjectPermissionSub.Member]: { title: "User Management", actions: [ - { label: "View all members", value: "read" }, - { label: "Invite members", value: "create" }, - { label: "Edit members", value: "edit" }, - { label: "Remove members", value: "delete" } + { label: "Read", value: ProjectPermissionMemberActions.Read }, + { label: "Add", value: ProjectPermissionMemberActions.Create }, + { label: "Modify", value: ProjectPermissionMemberActions.Edit }, + { label: "Remove", value: ProjectPermissionMemberActions.Delete }, + { label: "Manage Privileges", value: ProjectPermissionMemberActions.ManagePrivileges } ] }, [ProjectPermissionSub.Identity]: { title: "Machine Identity Management", actions: [ - { label: "Read", value: "read" }, - { label: "Add", value: "create" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { label: "Read", value: ProjectPermissionIdentityActions.Read }, + { label: "Add", value: ProjectPermissionIdentityActions.Create }, + { label: "Modify", value: ProjectPermissionIdentityActions.Edit }, + { label: "Remove", value: ProjectPermissionIdentityActions.Delete }, + { label: "Manage Privileges", value: ProjectPermissionIdentityActions.ManagePrivileges } ] }, [ProjectPermissionSub.Groups]: { title: "Group Management", actions: [ - { label: "Read", value: "read" }, - { label: "Create", value: "create" }, - { label: "Modify", value: "edit" }, - { label: "Remove", value: "delete" } + { label: "Read", value: ProjectPermissionGroupActions.Read }, + { label: "Create", value: ProjectPermissionGroupActions.Create }, + { label: "Modify", value: ProjectPermissionGroupActions.Edit }, + { label: "Remove", value: ProjectPermissionGroupActions.Delete }, + { label: "Manage Privileges", value: ProjectPermissionGroupActions.ManagePrivileges } ] }, [ProjectPermissionSub.Webhooks]: { From 9c702b27b2380ef4b4d6a67db3c195dd3e67016c Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Mon, 10 Mar 2025 16:24:55 +0800 Subject: [PATCH 05/27] misc: updated permission usage across FE --- backend/src/ee/services/group/group-service.ts | 4 ++-- backend/src/services/org/org-service.ts | 12 ++++++++---- .../services/project-key/project-key-service.ts | 6 +++--- .../src/context/OrgPermissionContext/index.tsx | 7 ++++++- .../context/ProjectPermissionContext/index.tsx | 3 +++ frontend/src/context/index.tsx | 5 +++++ .../AccessManagementPage/AccessManagementPage.tsx | 15 ++++++++++++--- .../OrgGroupsSection/OrgGroupsSection.tsx | 4 ++-- .../OrgGroupsSection/OrgGroupsTable.tsx | 10 +++++----- .../IdentitySection/IdentitySection.tsx | 9 ++++++--- .../components/IdentitySection/IdentityTable.tsx | 8 ++++---- .../GroupDetailsByIDPage/GroupDetailsByIDPage.tsx | 14 ++++++++++---- .../components/AddGroupMemberModal.tsx | 4 ++-- .../components/GroupDetailsSection.tsx | 4 ++-- .../GroupMembersSection/GroupMembersSection.tsx | 4 ++-- .../GroupMembersSection/GroupMembershipRow.tsx | 4 ++-- .../IdentityDetailsByIDPage.tsx | 4 ++-- .../IdentityAuthenticationSection.tsx | 4 ++-- .../IdentityClientSecrets.tsx | 4 ++-- .../components/IdentityDetailsSection.tsx | 12 +++++++++--- .../IdentityTokenAuthTokensTable.tsx | 8 ++++---- .../IdentityUniversalAuthClientSecretsTable.tsx | 6 +++--- .../ViewIdentityContentWrapper.tsx | 12 +++++++++--- .../UserDetailsByIDPage/UserDetailsByIDPage.tsx | 2 +- .../components/UserDetailsSection.tsx | 4 ++-- 25 files changed, 108 insertions(+), 61 deletions(-) diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 52f897022..3dcb7588f 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -349,7 +349,7 @@ export const groupServiceFactory = ({ // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups, permission, groupRolePermission @@ -425,7 +425,7 @@ export const groupServiceFactory = ({ // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( - OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups, permission, groupRolePermission diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index b0e5c1f67..182142e07 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -19,9 +19,13 @@ import { import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + OrgPermissionActions, + OrgPermissionGroupActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { getConfig } from "@app/lib/config/env"; @@ -183,7 +187,7 @@ export const orgServiceFactory = ({ const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups); const groups = await groupDAL.findByOrgId(orgId); return groups; }; @@ -845,7 +849,7 @@ export const orgServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(projectPermission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member ); const existingMembers = await projectMembershipDAL.find( diff --git a/backend/src/services/project-key/project-key-service.ts b/backend/src/services/project-key/project-key-service.ts index b56c0a43e..8ce2569a0 100644 --- a/backend/src/services/project-key/project-key-service.ts +++ b/backend/src/services/project-key/project-key-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; @@ -40,7 +40,7 @@ export const projectKeyServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); const receiverMembership = await projectMembershipDAL.findOne({ userId: receiverId, @@ -89,7 +89,7 @@ export const projectKeyServiceFactory = ({ actorOrgId, actionProjectType: ActionProjectType.Any }); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member); return projectKeyDAL.findAllProjectUserPubKeys(projectId); }; diff --git a/frontend/src/context/OrgPermissionContext/index.tsx b/frontend/src/context/OrgPermissionContext/index.tsx index 300f05904..acbbf3902 100644 --- a/frontend/src/context/OrgPermissionContext/index.tsx +++ b/frontend/src/context/OrgPermissionContext/index.tsx @@ -1,3 +1,8 @@ export { useOrgPermission } from "./OrgPermissionContext"; export type { TOrgPermission } from "./types"; -export { OrgPermissionActions, OrgPermissionSubjects } from "./types"; +export { + OrgPermissionActions, + OrgPermissionGroupActions, + OrgPermissionIdentityActions, + OrgPermissionSubjects +} from "./types"; diff --git a/frontend/src/context/ProjectPermissionContext/index.tsx b/frontend/src/context/ProjectPermissionContext/index.tsx index 69bcd4f99..5bc163817 100644 --- a/frontend/src/context/ProjectPermissionContext/index.tsx +++ b/frontend/src/context/ProjectPermissionContext/index.tsx @@ -4,6 +4,9 @@ export { ProjectPermissionActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, + ProjectPermissionGroupActions, + ProjectPermissionIdentityActions, ProjectPermissionKmipActions, + ProjectPermissionMemberActions, ProjectPermissionSub } from "./types"; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx index fb9d8c385..51f2797d0 100644 --- a/frontend/src/context/index.tsx +++ b/frontend/src/context/index.tsx @@ -2,6 +2,8 @@ export { useOrganization } from "./OrganizationContext"; export type { TOrgPermission } from "./OrgPermissionContext"; export { OrgPermissionActions, + OrgPermissionGroupActions, + OrgPermissionIdentityActions, OrgPermissionSubjects, useOrgPermission } from "./OrgPermissionContext"; @@ -10,7 +12,10 @@ export { ProjectPermissionActions, ProjectPermissionCmekActions, ProjectPermissionDynamicSecretActions, + ProjectPermissionGroupActions, + ProjectPermissionIdentityActions, ProjectPermissionKmipActions, + ProjectPermissionMemberActions, ProjectPermissionSub, useProjectPermission } from "./ProjectPermissionContext"; diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index 2aa8f2748..7eb59641b 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -4,7 +4,13 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; -import { OrgPermissionActions, OrgPermissionSubjects, useOrgPermission } from "@app/context"; +import { + OrgPermissionActions, + OrgPermissionGroupActions, + OrgPermissionIdentityActions, + OrgPermissionSubjects, + useOrgPermission +} from "@app/context"; import { OrgAccessControlTabSections } from "@app/types/org"; import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./components"; @@ -37,13 +43,16 @@ export const AccessManagementPage = () => { { key: OrgAccessControlTabSections.Groups, label: "Groups", - isHidden: permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Groups), + isHidden: permission.cannot(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups), component: OrgGroupsTab }, { key: OrgAccessControlTabSections.Identities, label: "Identities", - isHidden: permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Identity), + isHidden: permission.cannot( + OrgPermissionIdentityActions.Read, + OrgPermissionSubjects.Identity + ), component: OrgIdentityTab }, { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index 4bdb62d60..cc50e018c 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -5,7 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { useDeleteGroup } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -58,7 +58,7 @@ export const OrgGroupsSection = () => {

Groups

- + {(isAllowed) => (
- + {(isAllowed) => (
- + {(isAllowed) => ( { )} - + {(isAllowed) => ( { diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx index bcebea5c7..0995830e5 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/AddGroupMemberModal.tsx @@ -20,7 +20,7 @@ import { THead, Tr } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context"; import { useDebounce, useResetPageHelper } from "@app/hooks"; import { useAddUserToGroup, useListGroupUsers } from "@app/hooks/api"; import { EFilterReturnedUsers } from "@app/hooks/api/groups/types"; @@ -124,7 +124,7 @@ export const AddGroupMembersModal = ({ popUp, handlePopUpToggle }: Props) => { {(isAllowed) => { diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupDetailsSection.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupDetailsSection.tsx index a2010f43d..8d844bafc 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupDetailsSection.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupDetailsSection.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OrgPermissionCan } from "@app/components/permissions"; import { IconButton, Spinner, Tooltip } from "@app/components/v2"; import { CopyButton } from "@app/components/v2/CopyButton"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context"; import { useGetGroupById } from "@app/hooks/api/"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -22,7 +22,7 @@ export const GroupDetailsSection = ({ groupId, handlePopUpOpen }: Props) => {

Group Details

- + {(isAllowed) => { return ( diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx index 04b8d0197..90ed8da2a 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx @@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useOidcManageGroupMembershipsEnabled, useRemoveUserFromGroup } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -54,7 +54,7 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {

Group Members

- + {(isAllowed) => ( - + {(isAllowed) => { return ( { diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx index 51d1ecf5b..467fc7dcc 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityAuthenticationSection/IdentityAuthenticationSection.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button } from "@app/components/v2"; -import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -47,7 +47,7 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P {!Object.values(IdentityAuthMethod).every((method) => data.identity.authMethods.includes(method) ) && ( - + {(isAllowed) => (
); })} - + {(isAllowed) => { return ( - + {(isAllowed) => ( )} - + {(isAllowed) => ( {(isAllowed) => (

User Details

{userId !== membership.user.id && ( - + {(isAllowed) => { return ( @@ -196,7 +196,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) => (membership.status === "invited" || membership.status === "verified") && membership.user.email && serverDetails?.emailConfigured && ( - + {(isAllowed) => { return ( +
+ )} + {tabSections diff --git a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx new file mode 100644 index 000000000..71cfe460f --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx @@ -0,0 +1,242 @@ +import { Controller, useForm } from "react-hook-form"; +import { + faCheck, + faCircleInfo, + faExclamationTriangle, + faWarning +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, Checkbox, Modal, ModalContent } from "@app/components/v2"; +import { useOrgPermission } from "@app/context"; +import { useUpgradePrivilegeSystem } from "@app/hooks/api"; + +const formSchema = z.object({ + isProjectPrivilegesUpdated: z.literal(true), + isOrgPrivilegesUpdated: z.literal(true), + isInfrastructureUpdated: z.literal(true), + acknowledgesPermanentChange: z.literal(true) +}); + +type Props = { + isOpen?: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => { + const { membership } = useOrgPermission(); + + const { + handleSubmit, + control, + formState: { isSubmitting } + } = useForm({ resolver: zodResolver(formSchema) }); + const { mutateAsync: upgradePrivilegeSystem } = useUpgradePrivilegeSystem(); + + const handlePrivilegeSystemUpgrade = async () => { + try { + await upgradePrivilegeSystem(); + + createNotification({ + text: "Privilege system upgrade completed", + type: "success" + }); + + onOpenChange(false); + } catch { + createNotification({ + text: "Failed to upgrade privilege system", + type: "error" + }); + } + }; + + const isAdmin = membership?.role === "admin"; + + return ( + + +
+

+ Introducing Permission-Based Privilege Management +

+

+ We've developed an improved privilege management system that enhances how access + controls work in your organization. +

+ +
+
+
+ +

How it works:

+
+
+

+ Legacy system: Users with higher privilege levels could modify + access for anyone below them. +

+

+ New system: Users need explicit permission to modify specific + access levels, providing targeted control. After upgrading, you'll need to grant + the new 'Manage Privileges' permission at organization or project level. +

+
+
+ +
+
+ +

Benefits:

+
+
+
    +
  • More granular control over who can modify access levels
  • +
  • Improved security through precise permission checks
  • +
+
+
+
+ +

+ This upgrade affects operations like updating roles, managing group memberships, and + modifying privileges across your organization and projects. +

+
+
+
+ +

Upgrade privilege system

+
+

+ Your existing access control workflows will continue to function. However, actions that + involve changing privileges or permissions will now use the new permission-based system, + requiring users to have explicit permission to modify access levels. +

+
+
+
+ +

IMPORTANT: THIS CHANGE IS PERMANENT

+
+

+ Once upgraded, your organization cannot revert to + the legacy privilege system. Please ensure you've completed all preparations before + proceeding. +

+
+ +
+

Required preparation checklist:

+ +
+ ( + + I have reviewed project-level privileges and updated them if necessary + + )} + /> + + ( + + I have reviewed organization-level privileges and updated them if necessary + + )} + /> + + ( + + I have checked Terraform configurations and API integrations for compatibility + with the new system + + )} + /> + + ( + + + I understand that this upgrade is permanent and cannot be reversed + + + )} + /> +
+
+ +
+
+
+
+ ); +}; From dc2358bbaadaae87e030be7ba583eac710577223 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 14 Mar 2025 01:59:07 +0800 Subject: [PATCH 10/27] misc: added privilege version checks --- .../src/ee/services/group/group-service.ts | 14 +++++++++---- ...project-additional-privilege-v2-service.ts | 9 +++++--- ...ty-project-additional-privilege-service.ts | 9 +++++--- .../ee/services/permission/permission-dal.ts | 21 ++++++++++++++----- .../ee/services/permission/permission-fns.ts | 20 ++++++++++++++---- .../services/permission/permission-service.ts | 9 ++++++-- ...oject-user-additional-privilege-service.ts | 6 ++++-- .../group-project/group-project-service.ts | 6 ++++-- .../identity-aws-auth-service.ts | 4 +++- .../identity-azure-auth-service.ts | 3 ++- .../identity-gcp-auth-service.ts | 3 ++- .../identity-jwt-auth-service.ts | 3 ++- .../identity-kubernetes-auth-service.ts | 3 ++- .../identity-oidc-auth-service.ts | 4 +++- .../identity-project-service.ts | 7 +++++-- .../identity-token-auth-service.ts | 9 +++++--- .../identity-ua/identity-ua-service.ts | 15 ++++++++----- .../src/services/identity/identity-service.ts | 12 +++++++++-- backend/src/services/org/org-service.ts | 5 +++-- .../project-membership-service.ts | 3 ++- 20 files changed, 119 insertions(+), 46 deletions(-) diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 3dcb7588f..d2e836745 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -67,7 +67,7 @@ export const groupServiceFactory = ({ const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -89,6 +89,7 @@ export const groupServiceFactory = ({ const isCustomRole = Boolean(customRole); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.ManagePrivileges, OrgPermissionSubjects.Groups, permission, @@ -138,13 +139,14 @@ export const groupServiceFactory = ({ }: TUpdateGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); const plan = await licenseService.getPlan(actorOrgId); @@ -167,6 +169,7 @@ export const groupServiceFactory = ({ const isCustomRole = Boolean(customOrgRole); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.ManagePrivileges, OrgPermissionSubjects.Groups, permission, @@ -313,7 +316,7 @@ export const groupServiceFactory = ({ const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -349,11 +352,13 @@ export const groupServiceFactory = ({ // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups, permission, groupRolePermission ); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", @@ -389,7 +394,7 @@ export const groupServiceFactory = ({ }: TRemoveUserFromGroupDTO) => { if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -425,6 +430,7 @@ export const groupServiceFactory = ({ // check if user has broader or equal to privileges than group const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups, permission, diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 03bbbd8eb..37c258dc4 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -67,7 +67,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId }) ); - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ + const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ actor: ActorType.IDENTITY, actorId: identityId, projectId: identityProjectMembership.projectId, @@ -80,6 +80,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, @@ -158,7 +159,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ ProjectPermissionIdentityActions.Edit, subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId }) ); - const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({ + const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({ actor: ActorType.IDENTITY, actorId: identityProjectMembership.identityId, projectId: identityProjectMembership.projectId, @@ -171,6 +172,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, @@ -237,7 +239,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}` }); - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId: identityProjectMembership.projectId, @@ -258,6 +260,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ actionProjectType: ActionProjectType.Any }); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 513a933bf..6d603d664 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -67,7 +67,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId: identityProjectMembership.projectId, @@ -94,6 +94,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, @@ -160,7 +161,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId: identityProjectMembership.projectId, @@ -187,6 +188,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, @@ -270,7 +272,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!identityProjectMembership) throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` }); - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId: identityProjectMembership.projectId, @@ -292,6 +294,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ actionProjectType: ActionProjectType.Any }); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index ca46442e9..9b23d6113 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -49,6 +49,7 @@ export const permissionDALFactory = (db: TDbClient) => { .join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) .select( selectAllTableCols(TableName.OrgMembership), + db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"), db.ref("permissions").withSchema(TableName.OrgRoles), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), @@ -70,7 +71,8 @@ export const permissionDALFactory = (db: TDbClient) => { OrgMembershipsSchema.extend({ permissions: z.unknown(), orgAuthEnforced: z.boolean().optional().nullable(), - customRoleSlug: z.string().optional().nullable() + customRoleSlug: z.string().optional().nullable(), + shouldUseNewPrivilegeSystem: z.boolean() }).parse(el), childrenMapper: [ { @@ -118,7 +120,9 @@ export const permissionDALFactory = (db: TDbClient) => { .select(selectAllTableCols(TableName.IdentityOrgMembership)) .select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced")) .select("permissions") + .select(db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization)) .first(); + return membership; } catch (error) { throw new DatabaseError({ error, name: "GetOrgIdentityPermission" }); @@ -668,7 +672,8 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("orgId").withSchema(TableName.Project), db.ref("type").withSchema(TableName.Project).as("projectType"), - db.ref("id").withSchema(TableName.Project).as("projectId") + db.ref("id").withSchema(TableName.Project).as("projectId"), + db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization) ); const [userPermission] = sqlNestRelationships({ @@ -684,7 +689,8 @@ export const permissionDALFactory = (db: TDbClient) => { groupMembershipCreatedAt, groupMembershipUpdatedAt, membershipUpdatedAt, - projectType + projectType, + shouldUseNewPrivilegeSystem }) => ({ orgId, orgAuthEnforced, @@ -694,7 +700,8 @@ export const permissionDALFactory = (db: TDbClient) => { projectType, id: membershipId || groupMembershipId, createdAt: membershipCreatedAt || groupMembershipCreatedAt, - updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt + updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt, + shouldUseNewPrivilegeSystem }), childrenMapper: [ { @@ -995,6 +1002,7 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id` ) + .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .leftJoin(TableName.IdentityMetadata, (queryBuilder) => { void queryBuilder .on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`) @@ -1012,6 +1020,7 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), db.ref("permissions").withSchema(TableName.ProjectRoles), + db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization), db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), db @@ -1045,7 +1054,8 @@ export const permissionDALFactory = (db: TDbClient) => { membershipUpdatedAt, orgId, identityName, - projectType + projectType, + shouldUseNewPrivilegeSystem }) => ({ id: membershipId, identityId, @@ -1055,6 +1065,7 @@ export const permissionDALFactory = (db: TDbClient) => { updatedAt: membershipUpdatedAt, orgId, projectType, + shouldUseNewPrivilegeSystem, // just a prefilled value orgAuthEnforced: false }), diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 3c88fd12d..a0a94c352 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -152,16 +152,28 @@ const escapeHandlebarsMissingMetadata = (obj: Record) => { // the new privilege management system is based on the actor having the appropriate permission to perform the privilege change, // regardless of the actor's privilege level. const validatePrivilegeChangeOperation = ( + shouldUseNewPrivilegeSystem: boolean, opAction: OrgPermissionSet[0] | ProjectPermissionSet[0], opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1], actorPermission: MongoAbility, managedPermission: MongoAbility ) => { - // first we ensure if the actor has the permission to manage the privilege - if (actorPermission.can(opAction, opSubject)) { + if (shouldUseNewPrivilegeSystem) { + if (actorPermission.can(opAction, opSubject)) { + return { + isValid: true, + missingPermissions: [] + }; + } + return { - isValid: true, - missingPermissions: [] + isValid: false, + missingPermissions: [ + { + action: opAction, + subject: opSubject + } + ] }; } diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index ddaf55d9d..2997dcf74 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -390,14 +390,18 @@ export const permissionServiceFactory = ({ const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), - membership: undefined + membership: { + shouldUseNewPrivilegeSystem: true + } }; }; type TProjectPermissionRT = T extends ActorType.SERVICE ? { permission: MongoAbility; - membership: undefined; + membership: { + shouldUseNewPrivilegeSystem: boolean; + }; hasRole: (arg: string) => boolean; } // service token doesn't have both membership and roles : { @@ -406,6 +410,7 @@ export const permissionServiceFactory = ({ orgAuthEnforced: boolean | null | undefined; orgId: string; roles: Array<{ role: string }>; + shouldUseNewPrivilegeSystem: boolean; }; hasRole: (role: string) => boolean; }; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 167ce0964..c6a1ac835 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -68,7 +68,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ actionProjectType: ActionProjectType.Any }); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member); - const { permission: targetUserPermission } = await permissionService.getProjectPermission({ + const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({ actor: ActorType.USER, actorId: projectMembership.userId, projectId: projectMembership.projectId, @@ -81,6 +81,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionMemberActions.ManagePrivileges, ProjectPermissionSub.Member, permission, @@ -155,7 +156,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'` }); - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId: projectMembership.projectId, @@ -177,6 +178,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ // @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionMemberActions.ManagePrivileges, ProjectPermissionSub.Member, permission, diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 445141540..37ffacfa5 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -70,7 +70,7 @@ export const groupProjectServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` }); - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -103,6 +103,7 @@ export const groupProjectServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionGroupActions.ManagePrivileges, ProjectPermissionSub.Groups, permission, @@ -253,7 +254,7 @@ export const groupProjectServiceFactory = ({ if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -275,6 +276,7 @@ export const groupProjectServiceFactory = ({ project.id ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionGroupActions.ManagePrivileges, ProjectPermissionSub.Groups, permission, diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index a11fa64ea..f80b1b2c3 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -322,7 +322,7 @@ export const identityAwsAuthServiceFactory = ({ message: "The identity does not have aws auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -340,11 +340,13 @@ export const identityAwsAuthServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, rolePermission ); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 2e7013852..56249c606 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -296,7 +296,7 @@ export const identityAzureAuthServiceFactory = ({ message: "The identity does not have azure auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -313,6 +313,7 @@ export const identityAzureAuthServiceFactory = ({ actorOrgId ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index cf8812980..11c5151e9 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -342,7 +342,7 @@ export const identityGcpAuthServiceFactory = ({ message: "The identity does not have gcp auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -359,6 +359,7 @@ export const identityGcpAuthServiceFactory = ({ actorOrgId ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index a3b3842dd..353d1e46f 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -498,7 +498,7 @@ export const identityJwtAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -517,6 +517,7 @@ export const identityJwtAuthServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 4a6467af5..bc7dda22e 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -471,7 +471,7 @@ export const identityKubernetesAuthServiceFactory = ({ message: "The identity does not have kubernetes auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -488,6 +488,7 @@ export const identityKubernetesAuthServiceFactory = ({ actorOrgId ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index f8deb63c7..bb0077664 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -410,7 +410,7 @@ export const identityOidcAuthServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -429,11 +429,13 @@ export const identityOidcAuthServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, rolePermission ); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index cabed7751..ae7f8f25b 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -53,7 +53,7 @@ export const identityProjectServiceFactory = ({ projectId, roles }: TCreateProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -91,6 +91,7 @@ export const identityProjectServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, @@ -166,7 +167,7 @@ export const identityProjectServiceFactory = ({ actorAuthMethod, actorOrgId }: TUpdateProjectIdentityDTO) => { - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -192,11 +193,13 @@ export const identityProjectServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionIdentityActions.ManagePrivileges, ProjectPermissionSub.Identity, permission, rolePermission ); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 4dcba6910..ab2216458 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -237,7 +237,7 @@ export const identityTokenAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, @@ -246,6 +246,7 @@ export const identityTokenAuthServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -295,7 +296,7 @@ export const identityTokenAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, @@ -304,6 +305,7 @@ export const identityTokenAuthServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -423,7 +425,7 @@ export const identityTokenAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, @@ -431,6 +433,7 @@ export const identityTokenAuthServiceFactory = ({ actorOrgId ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 77e6ad3f3..0c067e057 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -360,7 +360,7 @@ export const identityUaServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); - const { permission: rolePermission } = await permissionService.getOrgPermission( + const { permission: rolePermission, membership } = await permissionService.getOrgPermission( ActorType.IDENTITY, identityMembershipOrg.identityId, identityMembershipOrg.orgId, @@ -368,6 +368,7 @@ export const identityUaServiceFactory = ({ actorOrgId ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity, permission, @@ -406,7 +407,7 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -423,6 +424,7 @@ export const identityUaServiceFactory = ({ actorOrgId ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity, permission, @@ -473,7 +475,7 @@ export const identityUaServiceFactory = ({ message: "The identity does not have universal auth" }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -491,6 +493,7 @@ export const identityUaServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity, permission, @@ -531,7 +534,7 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -548,6 +551,7 @@ export const identityUaServiceFactory = ({ actorOrgId ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity, permission, @@ -581,7 +585,7 @@ export const identityUaServiceFactory = ({ }); } - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityMembershipOrg.orgId, @@ -599,6 +603,7 @@ export const identityUaServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity, permission, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 41252bf25..937fba6f7 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -49,7 +49,13 @@ export const identityServiceFactory = ({ actorOrgId, metadata }: TCreateIdentityDTO) => { - const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); + const { permission, membership } = await permissionService.getOrgPermission( + actor, + actorId, + orgId, + actorAuthMethod, + actorOrgId + ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( @@ -58,6 +64,7 @@ export const identityServiceFactory = ({ ); const isCustomRole = Boolean(customRole); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.ManagePrivileges, OrgPermissionSubjects.Identity, permission, @@ -121,7 +128,7 @@ export const identityServiceFactory = ({ const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id }); if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` }); - const { permission } = await permissionService.getOrgPermission( + const { permission, membership } = await permissionService.getOrgPermission( actor, actorId, identityOrgMembership.orgId, @@ -139,6 +146,7 @@ export const identityServiceFactory = ({ const isCustomRole = Boolean(customOrgRole); const appliedRolePermissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, OrgPermissionIdentityActions.ManagePrivileges, OrgPermissionSubjects.Identity, permission, diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index b89b6f7fa..63aaa2457 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -291,7 +291,7 @@ export const orgServiceFactory = ({ }: TUpgradePrivilegeSystemDTO) => { const { membership } = await permissionService.getUserOrgPermission(actorId, orgId, actorAuthMethod, actorOrgId); - if (membership.role != OrgMembershipRole.Admin) { + if (membership.role !== OrgMembershipRole.Admin) { throw new ForbiddenRequestError({ message: "Insufficient privileges - only the organization admin can upgrade the privilege system." }); @@ -881,7 +881,7 @@ export const orgServiceFactory = ({ // if there exist no project membership we set is as given by the request for await (const project of projectsToInvite) { const projectId = project.id; - const { permission: projectPermission } = await permissionService.getProjectPermission({ + const { permission: projectPermission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -920,6 +920,7 @@ export const orgServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionMemberActions.ManagePrivileges, ProjectPermissionSub.Member, projectPermission, diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index ecc7f83d0..62668ed77 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -253,7 +253,7 @@ export const projectMembershipServiceFactory = ({ membershipId, roles }: TUpdateProjectMembershipDTO) => { - const { permission } = await permissionService.getProjectPermission({ + const { permission, membership } = await permissionService.getProjectPermission({ actor, actorId, projectId, @@ -275,6 +275,7 @@ export const projectMembershipServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( + membership.shouldUseNewPrivilegeSystem, ProjectPermissionMemberActions.ManagePrivileges, ProjectPermissionSub.Member, permission, From bdb7cb4cbfb6aeb1bcf5d6e0665eeb8c0ce88bd9 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 14 Mar 2025 03:29:49 +0800 Subject: [PATCH 11/27] misc: improved permission error message --- .../src/ee/services/group/group-service.ts | 31 +++++++++++--- ...project-additional-privilege-v2-service.ts | 23 +++++++++-- ...ty-project-additional-privilege-service.ts | 23 +++++++++-- .../ee/services/permission/permission-fns.ts | 17 +++++++- ...oject-user-additional-privilege-service.ts | 16 ++++++-- .../group-project/group-project-service.ts | 19 +++++++-- .../identity-aws-auth-service.ts | 12 +++++- .../identity-azure-auth-service.ts | 12 +++++- .../identity-gcp-auth-service.ts | 12 +++++- .../identity-jwt-auth-service.ts | 12 +++++- .../identity-kubernetes-auth-service.ts | 12 +++++- .../identity-oidc-auth-service.ts | 12 +++++- .../identity-project-service.ts | 19 +++++++-- .../identity-token-auth-service.ts | 26 ++++++++++-- .../identity-ua/identity-ua-service.ts | 40 ++++++++++++++++--- .../src/services/identity/identity-service.ts | 19 +++++++-- backend/src/services/org/org-service.ts | 12 +++++- .../project-membership-service.ts | 12 +++++- 18 files changed, 277 insertions(+), 52 deletions(-) diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index d2e836745..68df7ca69 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -14,7 +14,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal"; import { TLicenseServiceFactory } from "../license/license-service"; import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission"; -import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TGroupDALFactory } from "./group-dal"; import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns"; @@ -95,10 +95,16 @@ export const groupServiceFactory = ({ permission, rolePermission ); + if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to create a more privileged group", + message: constructPermissionErrorMessage( + "Failed to create a more privileged group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionSubjects.Groups + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -178,7 +184,12 @@ export const groupServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update a more privileged group", + message: constructPermissionErrorMessage( + "Failed to update a more privileged group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionSubjects.Groups + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); if (isCustomRole) customRole = customOrgRole; @@ -362,7 +373,12 @@ export const groupServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to add user to more privileged group", + message: constructPermissionErrorMessage( + "Failed to add user to more privileged group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.AddMembers, + OrgPermissionSubjects.Groups + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -439,7 +455,12 @@ export const groupServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to delete user from more privileged group", + message: constructPermissionErrorMessage( + "Failed to delete user from more privileged group", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionGroupActions.RemoveMembers, + OrgPermissionSubjects.Groups + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 37c258dc4..aaecefb7f 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -9,7 +9,7 @@ import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission"; import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal"; @@ -89,7 +89,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update more privileged identity", + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -181,7 +186,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update more privileged identity", + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -269,7 +279,12 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update more privileged identity", + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index 6d603d664..35e26d942 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -9,7 +9,7 @@ import { ActorType } from "@app/services/auth/auth-type"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal"; -import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionIdentityActions, @@ -103,7 +103,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update more privileged identity", + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -197,7 +202,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update more privileged identity", + message: constructPermissionErrorMessage( + "Failed to update more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -303,7 +313,12 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to edit more privileged identity", + message: constructPermissionErrorMessage( + "Failed to edit more privileged identity", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index a0a94c352..f8f2fa0b8 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -181,4 +181,19 @@ const validatePrivilegeChangeOperation = ( return validatePermissionBoundary(actorPermission, managedPermission); }; -export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO, validatePrivilegeChangeOperation }; +const constructPermissionErrorMessage = ( + baseMessage: string, + shouldUseNewPrivilegeSystem: boolean, + opAction: OrgPermissionSet[0] | ProjectPermissionSet[0], + opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1] +) => { + return `${baseMessage}${shouldUseNewPrivilegeSystem ? `. Missing permission ${opAction} on ${opSubject}` : ""}`; +}; + +export { + escapeHandlebarsMissingMetadata, + isAuthMethodSaml, + validateOrgSSO, + validatePrivilegeChangeOperation, + constructPermissionErrorMessage +}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index c6a1ac835..cddc131c9 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -8,7 +8,7 @@ import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/per import { ActorType } from "@app/services/auth/auth-type"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; -import { validatePrivilegeChangeOperation } from "../permission/permission-fns"; +import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionMemberActions, @@ -90,7 +90,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update more privileged user", + message: constructPermissionErrorMessage( + "Failed to update more privileged user", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionSub.Member + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -187,7 +192,12 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update more privileged user", + message: constructPermissionErrorMessage( + "Failed to update more privileged user", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionSub.Member + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 37ffacfa5..05193cc92 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -2,7 +2,10 @@ import { ForbiddenError } from "@casl/ability"; import ms from "ms"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; @@ -112,7 +115,12 @@ export const groupProjectServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to assign group to a more privileged role", + message: constructPermissionErrorMessage( + "Failed to assign group to a more privileged role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionSub.Groups + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); } @@ -285,7 +293,12 @@ export const groupProjectServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to assign group to a more privileged role", + message: constructPermissionErrorMessage( + "Failed to assign group to a more privileged role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionSub.Groups + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); } diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index f80b1b2c3..ddef0c553 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -6,7 +6,10 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -350,7 +353,12 @@ export const identityAwsAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke aws auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke aws auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 56249c606..61f9ca23f 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -4,7 +4,10 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -322,7 +325,12 @@ export const identityAzureAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke azure auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke azure auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 11c5151e9..014e4619f 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -4,7 +4,10 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -368,7 +371,12 @@ export const identityGcpAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke gcp auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke gcp auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 353d1e46f..114ca79c0 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -6,7 +6,10 @@ import { JwksClient } from "jwks-rsa"; import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -526,7 +529,12 @@ export const identityJwtAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke jwt auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke jwt auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index bc7dda22e..a18de9ad8 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -6,7 +6,10 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -497,7 +500,12 @@ export const identityKubernetesAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke kubernetes auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke kubernetes auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index bb0077664..3e58c8f6f 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -7,7 +7,10 @@ import { JwksClient } from "jwks-rsa"; import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -439,7 +442,12 @@ export const identityOidcAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke oidc auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke oidc auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index ae7f8f25b..5486b37a8 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -2,7 +2,10 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -100,7 +103,12 @@ export const identityProjectServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to assign to a more privileged role", + message: constructPermissionErrorMessage( + "Failed to assign to a more privileged role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); } @@ -203,7 +211,12 @@ export const identityProjectServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to change to a more privileged role", + message: constructPermissionErrorMessage( + "Failed to change to a more privileged role", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionSub.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); } diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index ab2216458..4079d0756 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -4,7 +4,10 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; @@ -255,7 +258,12 @@ export const identityTokenAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke token auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke token auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -314,7 +322,12 @@ export const identityTokenAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to create token for identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to create token for identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -442,7 +455,12 @@ export const identityTokenAuthServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to update token for identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to update token for identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 0c067e057..c057a656a 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -7,7 +7,10 @@ import jwt from "jsonwebtoken"; import { IdentityAuthMethod } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; @@ -377,7 +380,12 @@ export const identityUaServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke universal auth of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke universal auth of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.RevokeAuth, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -433,7 +441,12 @@ export const identityUaServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to create client secret for a more privileged identity.", + message: constructPermissionErrorMessage( + "Failed to create client secret for a more privileged identity.", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -502,7 +515,12 @@ export const identityUaServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to get identity client secret with more privileged role", + message: constructPermissionErrorMessage( + "Failed to get identity client secret with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -560,7 +578,12 @@ export const identityUaServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to read identity client secret of identity with more privileged role", + message: constructPermissionErrorMessage( + "Failed to read identity client secret of identity with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.GetToken, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -612,7 +635,12 @@ export const identityUaServiceFactory = ({ if (!permissionBoundary.isValid) { throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to revoke identity client secret with more privileged role", + message: constructPermissionErrorMessage( + "Failed to revoke identity client secret with more privileged role", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.DeleteToken, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); } diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 937fba6f7..0426e6f8c 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -3,7 +3,10 @@ import { ForbiddenError } from "@casl/ability"; import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; @@ -73,7 +76,12 @@ export const identityServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to create a more privileged identity", + message: constructPermissionErrorMessage( + "Failed to create a more privileged identity", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); @@ -155,7 +163,12 @@ export const identityServiceFactory = ({ if (!appliedRolePermissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to create a more privileged identity", + message: constructPermissionErrorMessage( + "Failed to update identity", + membership.shouldUseNewPrivilegeSystem, + OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionSubjects.Identity + ), details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } }); if (isCustomRole) customRole = customOrgRole; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 63aaa2457..844d3f385 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -24,7 +24,10 @@ import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; @@ -930,7 +933,12 @@ export const orgServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: "Failed to invite user to a more privileged role in the project", + message: constructPermissionErrorMessage( + "Failed to invite user to a more privileged role in the project", + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionSub.Member + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); } diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 62668ed77..2708e69ba 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -4,7 +4,10 @@ import ms from "ms"; import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; +import { + constructPermissionErrorMessage, + validatePrivilegeChangeOperation +} from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; @@ -284,7 +287,12 @@ export const projectMembershipServiceFactory = ({ if (!permissionBoundary.isValid) throw new ForbiddenRequestError({ name: "PermissionBoundaryError", - message: `Failed to change to a more privileged role ${requestedRoleChange}`, + message: constructPermissionErrorMessage( + `Failed to change to a more privileged role ${requestedRoleChange}`, + membership.shouldUseNewPrivilegeSystem, + ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionSub.Member + ), details: { missingPermissions: permissionBoundary.missingPermissions } }); } From 430f8458cbe3fc9386df30de013c4570c3951329 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 14 Mar 2025 03:47:12 +0800 Subject: [PATCH 12/27] misc: minor format --- .../ee/services/permission/permission-fns.ts | 4 +++- backend/src/services/org/org-service.ts | 4 ++-- .../UpgradePrivilegeSystemModal.tsx | 19 +++++++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index f8f2fa0b8..f8a36bcad 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -187,7 +187,9 @@ const constructPermissionErrorMessage = ( opAction: OrgPermissionSet[0] | ProjectPermissionSet[0], opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1] ) => { - return `${baseMessage}${shouldUseNewPrivilegeSystem ? `. Missing permission ${opAction} on ${opSubject}` : ""}`; + return `${baseMessage}${ + shouldUseNewPrivilegeSystem ? `. Missing permission ${opAction as string} on ${opSubject as string}` : "" + }`; }; export { diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 844d3f385..9b4680317 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -1076,9 +1076,9 @@ export const orgServiceFactory = ({ const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = []; invitedProjectRoles.forEach((projectRole) => { const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]); - projectMemberships.forEach((membership) => { + projectMemberships.forEach((membershipEntry) => { sanitizedProjectMembershipRoles.push({ - projectMembershipId: membership.id, + projectMembershipId: membershipEntry.id, role: isCustomRole ? ProjectMembershipRole.Custom : projectRole, customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null }); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx index 71cfe460f..aa505965b 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx @@ -64,7 +64,7 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => Introducing Permission-Based Privilege Management

- We've developed an improved privilege management system that enhances how access + We've developed an improved privilege management system that enhances how access controls work in your organization.

@@ -75,19 +75,22 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) =>

How it works:

-

+

Legacy system: Users with higher privilege levels could modify - access for anyone below them. + access for anyone below them. This limits accountability and makes it impossible + to allow specialized roles (like team leads) to manage their team's access + without granting them broader administrative powers.

New system: Users need explicit permission to modify specific - access levels, providing targeted control. After upgrading, you'll need to grant - the new 'Manage Privileges' permission at organization or project level. + access levels, providing targeted control. After upgrading, you'll need to + grant the new 'Manage Privileges' permission at organization or project + level.

-
+

Benefits:

@@ -128,8 +131,8 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) =>

Once upgraded, your organization cannot revert to - the legacy privilege system. Please ensure you've completed all preparations before - proceeding. + the legacy privilege system. Please ensure you've completed all preparations + before proceeding.

From 2149c0a9d1676f3cd91d93845823590b825577a6 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 14 Mar 2025 03:50:17 +0800 Subject: [PATCH 13/27] misc: added neat check all condition --- .../UpgradePrivilegeSystemModal.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx index aa505965b..05deb45d0 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx @@ -32,10 +32,21 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => const { handleSubmit, control, + watch, formState: { isSubmitting } } = useForm({ resolver: zodResolver(formSchema) }); const { mutateAsync: upgradePrivilegeSystem } = useUpgradePrivilegeSystem(); + const isProjectPrivilegesUpdated = watch("isProjectPrivilegesUpdated"); + const isOrgPrivilegesUpdated = watch("isOrgPrivilegesUpdated"); + const isInfrastructureUpdated = watch("isInfrastructureUpdated"); + const acknowledgesPermanentChange = watch("acknowledgesPermanentChange"); + const isAllChecksCompleted = + isProjectPrivilegesUpdated && + isOrgPrivilegesUpdated && + isInfrastructureUpdated && + acknowledgesPermanentChange; + const handlePrivilegeSystemUpgrade = async () => { try { await upgradePrivilegeSystem(); @@ -231,7 +242,7 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) =>

- We've developed an improved privilege management system to better serve your security - needs. Upgrade to our new permission-based approach that allows you to explicitly - designate who can modify specific access levels, rather than relying on traditional - hierarchy comparisons. + We've developed an improved privilege management system to better serve your + security needs. Upgrade to our new permission-based approach that allows you to + explicitly designate who can modify specific access levels, rather than relying on + traditional hierarchy comparisons.

- - - + + ) : ( +
+

+ Your existing access control workflows will continue to function. However, actions that + involve modifying privileges or permissions will now use the new permission-based system, + requiring users to have explicit permission to modify given resource. +

+ +

+ This upgrade affects operations like updating roles, managing group memberships, and + modifying privileges across your organization and projects. +

+ +
+

+ Once upgraded, your organization cannot revert to the + legacy privilege system. Please ensure you've completed all preparations before + proceeding. +

+ +
+

Required preparation checklist:

+ +
+ ( + + I have reviewed project-level privileges and updated them if necessary + + )} + /> + ( + + I have reviewed organization-level privileges and updated them if necessary + + )} + /> + ( + + I have checked Terraform configurations and API integrations for compatibility + with the new system + + )} + /> + ( + + I understand that this upgrade is permanent and cannot be reversed + + )} + /> +
+
+
+ +
+
+ + +
+
+
+ )} ); From 956d0f6c5d0ca36c25509b4ff2d2cfb8f0bea6d4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 27 Mar 2025 12:17:36 +0800 Subject: [PATCH 25/27] misc: review comments --- .../src/ee/services/group/group-service.ts | 22 +-- ...project-additional-privilege-v2-service.ts | 23 +-- ...ty-project-additional-privilege-service.ts | 23 +-- .../ee/services/permission/org-permission.ts | 8 +- .../services/permission/project-permission.ts | 12 +- ...oject-user-additional-privilege-service.ts | 16 +- backend/src/lib/errors/index.ts | 17 ++ backend/src/server/plugins/error-handler.ts | 3 +- .../group-project/group-project-service.ts | 16 +- .../identity-aws-auth-service.ts | 5 +- .../identity-azure-auth-service.ts | 5 +- .../identity-gcp-auth-service.ts | 5 +- .../identity-jwt-auth-service.ts | 11 +- .../identity-kubernetes-auth-service.ts | 5 +- .../identity-oidc-auth-service.ts | 11 +- .../identity-project-service.ts | 16 +- .../identity-token-auth-service.ts | 11 +- .../identity-ua/identity-ua-service.ts | 17 +- .../src/services/identity/identity-service.ts | 16 +- backend/src/services/org/org-service.ts | 15 +- .../project-membership-service.ts | 9 +- .../permissions/organization-permissions.mdx | 41 +++-- .../permissions/project-permissions.mdx | 111 +++++++---- .../src/context/OrgPermissionContext/types.ts | 4 +- .../context/ProjectPermissionContext/types.ts | 6 +- .../UpgradePrivilegeSystemModal.tsx | 174 +++++++++--------- .../components/OrgRoleModifySection.utils.ts | 4 +- .../OrgPermissionGroupRow.tsx | 10 +- .../OrgPermissionIdentityRow.tsx | 10 +- .../ProjectRoleModifySection.utils.tsx | 32 ++-- 30 files changed, 346 insertions(+), 312 deletions(-) diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 916e1eee1..be8abf0cd 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -3,7 +3,7 @@ import slugify from "@sindresorhus/slugify"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; @@ -90,19 +90,18 @@ export const groupServiceFactory = ({ const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create group", membership.shouldUseNewPrivilegeSystem, - OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -176,18 +175,17 @@ export const groupServiceFactory = ({ const isCustomRole = Boolean(customOrgRole); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update group", membership.shouldUseNewPrivilegeSystem, - OrgPermissionGroupActions.ManagePrivileges, + OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -371,8 +369,7 @@ export const groupServiceFactory = ({ ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to add user to more privileged group", membership.shouldUseNewPrivilegeSystem, @@ -453,8 +450,7 @@ export const groupServiceFactory = ({ groupRolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to delete user from more privileged group", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts index 32c32fbe3..e1375744e 100644 --- a/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability"; import { packRules } from "@casl/ability/extra"; import { ActionProjectType, TableName } from "@app/db/schemas"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; @@ -81,18 +81,17 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, targetIdentityPermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update more privileged identity", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -178,18 +177,17 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, targetIdentityPermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update more privileged identity", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -271,18 +269,17 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({ }); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, identityRolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update more privileged identity", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts index be3d60e6a..486ea3451 100644 --- a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability" import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { ActionProjectType } from "@app/db/schemas"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; @@ -95,18 +95,17 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission)); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, targetIdentityPermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update more privileged identity", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -194,18 +193,17 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || [])); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, targetIdentityPermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update more privileged identity", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -305,18 +303,17 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({ }); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, identityRolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to edit more privileged identity", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 8b0292664..17b4e7f6c 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -49,7 +49,7 @@ export enum OrgPermissionIdentityActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges", + GrantPrivileges = "grant-privileges", RevokeAuth = "revoke-auth", CreateToken = "create-token", GetToken = "get-token", @@ -61,7 +61,7 @@ export enum OrgPermissionGroupActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges", + GrantPrivileges = "grant-privileges", AddMembers = "add-members", RemoveMembers = "remove-members" } @@ -282,7 +282,7 @@ const buildAdminPermission = () => { can(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups); - can(OrgPermissionGroupActions.ManagePrivileges, OrgPermissionSubjects.Groups); + can(OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups); can(OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups); @@ -295,7 +295,7 @@ const buildAdminPermission = () => { can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); - can(OrgPermissionIdentityActions.ManagePrivileges, OrgPermissionSubjects.Identity); + can(OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity); can(OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity); diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 95d16e473..2637db994 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -48,7 +48,7 @@ export enum ProjectPermissionIdentityActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges" + GrantPrivileges = "grant-privileges" } export enum ProjectPermissionMemberActions { @@ -56,7 +56,7 @@ export enum ProjectPermissionMemberActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges" + GrantPrivileges = "grant-privileges" } export enum ProjectPermissionGroupActions { @@ -64,7 +64,7 @@ export enum ProjectPermissionGroupActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges" + GrantPrivileges = "grant-privileges" } export enum ProjectPermissionSecretSyncActions { @@ -590,7 +590,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionMemberActions.Edit, ProjectPermissionMemberActions.Delete, ProjectPermissionMemberActions.Read, - ProjectPermissionMemberActions.ManagePrivileges + ProjectPermissionMemberActions.GrantPrivileges ], ProjectPermissionSub.Member ); @@ -601,7 +601,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionGroupActions.Edit, ProjectPermissionGroupActions.Delete, ProjectPermissionGroupActions.Read, - ProjectPermissionGroupActions.ManagePrivileges + ProjectPermissionGroupActions.GrantPrivileges ], ProjectPermissionSub.Groups ); @@ -612,7 +612,7 @@ const buildAdminPermissionRules = () => { ProjectPermissionIdentityActions.Edit, ProjectPermissionIdentityActions.Delete, ProjectPermissionIdentityActions.Read, - ProjectPermissionIdentityActions.ManagePrivileges + ProjectPermissionIdentityActions.GrantPrivileges ], ProjectPermissionSub.Identity ); diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts index 388786e82..c592b7afa 100644 --- a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { ActionProjectType, TableName } from "@app/db/schemas"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission"; import { ActorType } from "@app/services/auth/auth-type"; @@ -82,18 +82,17 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ targetUserPermission.update(targetUserPermission.rules.concat(customPermission)); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member, permission, targetUserPermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update more privileged user", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -184,18 +183,17 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({ targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || [])); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member, permission, targetUserPermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update more privileged user", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member ), details: { missingPermissions: permissionBoundary.missingPermissions } diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index 950bfd29f..a5df64caf 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -68,6 +68,23 @@ export class ForbiddenRequestError extends Error { } } +export class PermissionBoundaryError extends ForbiddenRequestError { + constructor({ + message, + name, + error, + details + }: { + message?: string; + name?: string; + error?: unknown; + details?: unknown; + }) { + super({ message, name, error, details }); + this.name = "PermissionBoundaryError"; + } +} + export class BadRequestError extends Error { name: string; diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 0fd18bc29..c8170a023 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -13,6 +13,7 @@ import { InternalServerError, NotFoundError, OidcAuthError, + PermissionBoundaryError, RateLimitError, ScimRequestError, UnauthorizedError @@ -117,7 +118,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider conditions: el.conditions })) }); - } else if (error instanceof ForbiddenRequestError) { + } else if (error instanceof ForbiddenRequestError || error instanceof PermissionBoundaryError) { void res.status(HttpStatusCodes.Forbidden).send({ reqId: req.id, statusCode: HttpStatusCodes.Forbidden, diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 4849b2083..1ff2c78d2 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -9,7 +9,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; import { isUuidV4 } from "@app/lib/validator"; @@ -107,18 +107,17 @@ export const groupProjectServiceFactory = ({ const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionGroupActions.GrantPrivileges, ProjectPermissionSub.Groups, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to assign group to role", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionGroupActions.GrantPrivileges, ProjectPermissionSub.Groups ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -285,18 +284,17 @@ export const groupProjectServiceFactory = ({ ); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionGroupActions.GrantPrivileges, ProjectPermissionSub.Groups, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to assign group to role", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionGroupActions.ManagePrivileges, + ProjectPermissionGroupActions.GrantPrivileges, ProjectPermissionSub.Groups ), details: { missingPermissions: permissionBoundary.missingPermissions } diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 8fa34f533..130cd6b74 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -12,7 +12,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -355,8 +355,7 @@ export const identityAwsAuthServiceFactory = ({ ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke aws auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 07a5c0d01..5baa00652 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -10,7 +10,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -327,8 +327,7 @@ export const identityAzureAuthServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke azure auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index d86f925b7..7420b61cf 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -10,7 +10,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -373,8 +373,7 @@ export const identityGcpAuthServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke gcp auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 82c9d06da..39fc28ad2 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -12,7 +12,13 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getStringValueByDot } from "@app/lib/template/dot-access"; @@ -533,8 +539,7 @@ export const identityJwtAuthServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke jwt auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index c61612bfb..c006567d8 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -12,7 +12,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -503,8 +503,7 @@ export const identityKubernetesAuthServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke kubernetes auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 002616f45..0d0aa4504 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -13,7 +13,13 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { getStringValueByDot } from "@app/lib/template/dot-access"; @@ -472,8 +478,7 @@ export const identityOidcAuthServiceFactory = ({ ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke oidc auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-project/identity-project-service.ts b/backend/src/services/identity-project/identity-project-service.ts index bba30d7bc..ba63f7afd 100644 --- a/backend/src/services/identity-project/identity-project-service.ts +++ b/backend/src/services/identity-project/identity-project-service.ts @@ -7,7 +7,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; @@ -95,18 +95,17 @@ export const identityProjectServiceFactory = ({ const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to assign to role", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -202,19 +201,18 @@ export const identityProjectServiceFactory = ({ const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to change role", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionIdentityActions.ManagePrivileges, + ProjectPermissionIdentityActions.GrantPrivileges, ProjectPermissionSub.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 7cfbfc592..549512452 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -10,7 +10,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -266,8 +266,7 @@ export const identityTokenAuthServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke token auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, @@ -333,8 +332,7 @@ export const identityTokenAuthServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create token for identity with more privileged role", membership.shouldUseNewPrivilegeSystem, @@ -472,8 +470,7 @@ export const identityTokenAuthServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update token for identity with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 117249580..701d87d36 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -13,7 +13,7 @@ import { } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -382,8 +382,7 @@ export const identityUaServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke universal auth of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, @@ -443,8 +442,7 @@ export const identityUaServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create client secret for identity.", membership.shouldUseNewPrivilegeSystem, @@ -517,8 +515,7 @@ export const identityUaServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to get identity client secret with more privileged role", membership.shouldUseNewPrivilegeSystem, @@ -580,8 +577,7 @@ export const identityUaServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to read identity client secret of identity with more privileged role", membership.shouldUseNewPrivilegeSystem, @@ -637,8 +633,7 @@ export const identityUaServiceFactory = ({ rolePermission ); if (!permissionBoundary.isValid) { - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to revoke identity client secret with more privileged role", membership.shouldUseNewPrivilegeSystem, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index d06572d04..f9185ba9d 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -8,7 +8,7 @@ import { validatePrivilegeChangeOperation } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; @@ -69,18 +69,17 @@ export const identityServiceFactory = ({ const isCustomRole = Boolean(customRole); const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to create identity", membership.shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity ), details: { missingPermissions: permissionBoundary.missingPermissions } @@ -159,18 +158,17 @@ export const identityServiceFactory = ({ const isCustomRole = Boolean(customOrgRole); const appliedRolePermissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity, permission, rolePermission ); if (!appliedRolePermissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to update identity", membership.shouldUseNewPrivilegeSystem, - OrgPermissionIdentityActions.ManagePrivileges, + OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity ), details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions } diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index eeb0e09ac..ba7d9dac9 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -37,7 +37,13 @@ import { getConfig } from "@app/lib/config/env"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; -import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + ForbiddenRequestError, + NotFoundError, + PermissionBoundaryError, + UnauthorizedError +} from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; @@ -941,19 +947,18 @@ export const orgServiceFactory = ({ const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member, projectPermission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( "Failed to invite user to the project", membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member ), details: { missingPermissions: permissionBoundary.missingPermissions } diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 98c2b1d3b..f452e381f 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -11,7 +11,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; import { ms } from "@app/lib/ms"; @@ -279,18 +279,17 @@ export const projectMembershipServiceFactory = ({ const permissionBoundary = validatePrivilegeChangeOperation( membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member, permission, rolePermission ); if (!permissionBoundary.isValid) - throw new ForbiddenRequestError({ - name: "PermissionBoundaryError", + throw new PermissionBoundaryError({ message: constructPermissionErrorMessage( `Failed to change role ${requestedRoleChange}`, membership.shouldUseNewPrivilegeSystem, - ProjectPermissionMemberActions.ManagePrivileges, + ProjectPermissionMemberActions.GrantPrivileges, ProjectPermissionSub.Member ), details: { missingPermissions: permissionBoundary.missingPermissions } diff --git a/docs/internals/permissions/organization-permissions.mdx b/docs/internals/permissions/organization-permissions.mdx index 2daa90c4e..c68d845e2 100644 --- a/docs/internals/permissions/organization-permissions.mdx +++ b/docs/internals/permissions/organization-permissions.mdx @@ -50,29 +50,29 @@ Below is a comprehensive list of all available organization-level subjects and t #### Subject: `groups` -| Action | Description | -| ------------------- | ------------------------------------------------ | -| `read` | View organization groups | -| `create` | Create new groups in the organization | -| `edit` | Modify existing groups | -| `delete` | Remove groups from the organization | -| `manage-privileges` | Change permission levels for organization groups | -| `add-members` | Add members to groups | -| `remove-members` | Remove members from groups | +| Action | Description | +| ------------------ | ------------------------------------------------ | +| `read` | View organization groups | +| `create` | Create new groups in the organization | +| `edit` | Modify existing groups | +| `delete` | Remove groups from the organization | +| `grant-privileges` | Change permission levels for organization groups | +| `add-members` | Add members to groups | +| `remove-members` | Remove members from groups | #### Subject: `identity` -| Action | Description | -| ------------------- | --------------------------------------------------- | -| `read` | View organization identities | -| `create` | Add new identities to organization | -| `edit` | Modify organization identities | -| `delete` | Remove identities from organization | -| `manage-privileges` | Change permission levels of organization identities | -| `revoke-auth` | Revoke authentication for identities | -| `create-token` | Create new authentication tokens | -| `delete-token` | Delete authentication tokens | -| `get-token` | Retrieve authentication tokens | +| Action | Description | +| ------------------ | --------------------------------------------------- | +| `read` | View organization identities | +| `create` | Add new identities to organization | +| `edit` | Modify organization identities | +| `delete` | Remove identities from organization | +| `grant-privileges` | Change permission levels of organization identities | +| `revoke-auth` | Revoke authentication for identities | +| `create-token` | Create new authentication tokens | +| `delete-token` | Delete authentication tokens | +| `get-token` | Retrieve authentication tokens | ### Security & Compliance @@ -163,6 +163,7 @@ Below is a comprehensive list of all available organization-level subjects and t ### Integrations #### Subject: `app-connections` + Supports conditions and permission inversion | Action | Description | diff --git a/docs/internals/permissions/project-permissions.mdx b/docs/internals/permissions/project-permissions.mdx index ea3b08fea..a24f6d782 100644 --- a/docs/internals/permissions/project-permissions.mdx +++ b/docs/internals/permissions/project-permissions.mdx @@ -21,6 +21,7 @@ Below is a comprehensive list of all available project-level subjects and their ### Core Platform & Access Control #### Subject: `role` + | Action | Description | | -------- | ------------------------------------------------- | | `read` | View project roles and their assigned permissions | @@ -29,33 +30,37 @@ Below is a comprehensive list of all available project-level subjects and their | `delete` | Remove project roles | #### Subject: `member` -| Action | Description | -| ------------------- | ------------------------------------------- | -| `read` | View project members | -| `create` | Add new members to the project | -| `edit` | Modify member details | -| `delete` | Remove members from the project | -| `manage-privileges` | Change permission levels of project members | + +| Action | Description | +| ------------------ | ------------------------------------------- | +| `read` | View project members | +| `create` | Add new members to the project | +| `edit` | Modify member details | +| `delete` | Remove members from the project | +| `grant-privileges` | Change permission levels of project members | #### Subject: `groups` -| Action | Description | -| ------------------- | ------------------------------------------ | -| `read` | View project groups | -| `create` | Create new groups within the project | -| `edit` | Modify existing groups | -| `delete` | Remove groups from the project | -| `manage-privileges` | Change permission levels of project groups | + +| Action | Description | +| ------------------ | ------------------------------------------ | +| `read` | View project groups | +| `create` | Create new groups within the project | +| `edit` | Modify existing groups | +| `delete` | Remove groups from the project | +| `grant-privileges` | Change permission levels of project groups | #### Subject: `identity` -| Action | Description | -| ------------------- | ---------------------------------------------- | -| `read` | View project identities | -| `create` | Add new identities to project | -| `edit` | Modify project identities | -| `delete` | Remove identities from project | -| `manage-privileges` | Change permission levels of project identities | + +| Action | Description | +| ------------------ | ---------------------------------------------- | +| `read` | View project identities | +| `create` | Add new identities to project | +| `edit` | Modify project identities | +| `delete` | Remove identities from project | +| `grant-privileges` | Change permission levels of project identities | #### Subject: `settings` + | Action | Description | | -------- | -------------------------------------- | | `read` | View project settings | @@ -64,6 +69,7 @@ Below is a comprehensive list of all available project-level subjects and their | `delete` | Remove project settings | #### Subject: `environments` + | Action | Description | | -------- | ------------------------------------ | | `read` | View project environments | @@ -72,6 +78,7 @@ Below is a comprehensive list of all available project-level subjects and their | `delete` | Remove environments from the project | #### Subject: `tags` + | Action | Description | | -------- | ---------------------------------------- | | `read` | View project tags | @@ -80,12 +87,14 @@ Below is a comprehensive list of all available project-level subjects and their | `delete` | Remove tags from the project | #### Subject: `workspace` + | Action | Description | | -------- | ------------------------- | | `edit` | Modify workspace settings | | `delete` | Delete the workspace | #### Subject: `ip-allowlist` + | Action | Description | | -------- | -------------------------------------------- | | `read` | View IP allowlists | @@ -94,11 +103,13 @@ Below is a comprehensive list of all available project-level subjects and their | `delete` | Remove IP addresses from allowlists | #### Subject: `audit-logs` + | Action | Description | | ------ | ------------------------------------------------------- | | `read` | View audit logs of actions performed within the project | #### Subject: `integrations` + | Action | Description | | -------- | -------------------------------- | | `read` | View configured integrations | @@ -107,6 +118,7 @@ Below is a comprehensive list of all available project-level subjects and their | `delete` | Remove integrations | #### Subject: `webhooks` + | Action | Description | | -------- | ------------------------------------ | | `read` | View webhook configurations | @@ -115,6 +127,7 @@ Below is a comprehensive list of all available project-level subjects and their | `delete` | Remove webhooks | #### Subject: `service-tokens` + | Action | Description | | -------- | ---------------------------------------- | | `read` | View service tokens | @@ -125,39 +138,44 @@ Below is a comprehensive list of all available project-level subjects and their ### Secrets Management #### Subject: `secrets` + Supports conditions and permission inversion -| Action | Description | +| Action | Description | | -------- | ------------------------------- | -| `read` | View secrets and their values | -| `create` | Add new secrets to the project | -| `edit` | Modify existing secret values | +| `read` | View secrets and their values | +| `create` | Add new secrets to the project | +| `edit` | Modify existing secret values | | `delete` | Remove secrets from the project | #### Subject: `secret-folders` + Supports conditions and permission inversion -| Action | Description | +| Action | Description | | -------- | ------------------------ | -| `read` | View secret folders | -| `create` | Create new folders | -| `edit` | Modify folder properties | -| `delete` | Remove secret folders | +| `read` | View secret folders | +| `create` | Create new folders | +| `edit` | Modify folder properties | +| `delete` | Remove secret folders | #### Subject: `secret-imports` + Supports conditions and permission inversion -| Action | Description | +| Action | Description | | -------- | --------------------- | -| `read` | View secret imports | +| `read` | View secret imports | | `create` | Create secret imports | -| `edit` | Modify secret imports | +| `edit` | Modify secret imports | | `delete` | Remove secret imports | #### Subject: `secret-rollback` + | Action | Description | | -------- | ---------------------------------- | | `read` | View secret versions and snapshots | | `create` | Roll back secrets to snapshots | #### Subject: `secret-approval` + | Action | Description | | -------- | ----------------------------------- | | `read` | View approval policies and requests | @@ -166,6 +184,7 @@ Supports conditions and permission inversion | `delete` | Remove approval policies | #### Subject: `secret-rotation` + | Action | Description | | -------- | ------------------------------------- | | `read` | View secret rotation policies | @@ -174,6 +193,7 @@ Supports conditions and permission inversion | `delete` | Remove rotation policies | #### Subject: `secret-syncs` + | Action | Description | | ---------------- | -------------------------------------------------- | | `read` | View secret synchronization configurations | @@ -185,23 +205,26 @@ Supports conditions and permission inversion | `remove-secrets` | Remove secrets from sync destinations | #### Subject: `dynamic-secrets` + Supports conditions and permission inversion -| Action | Description | +| Action | Description | | ------------------------ | ---------------------------------- | -| `read-root-credential` | View dynamic secret configurations | -| `create-root-credential` | Create dynamic secrets | -| `edit-root-credential` | Edit dynamic secrets | -| `delete-root-credential` | Remove dynamic secrets | -| `lease` | Create dynamic secret leases | +| `read-root-credential` | View dynamic secret configurations | +| `create-root-credential` | Create dynamic secrets | +| `edit-root-credential` | Edit dynamic secrets | +| `delete-root-credential` | Remove dynamic secrets | +| `lease` | Create dynamic secret leases | ### Key Management Service (KMS) #### Subject: `kms` + | Action | Description | | ------ | --------------------------- | | `edit` | Modify project KMS settings | #### Subject: `cmek` + | Action | Description | | --------- | ------------------------------------- | | `read` | View Customer-Managed Encryption Keys | @@ -214,6 +237,7 @@ Supports conditions and permission inversion ### Public Key Infrastructure (PKI) #### Subject: `certificate-authorities` + | Action | Description | | -------- | ---------------------------------- | | `read` | View certificate authorities | @@ -222,6 +246,7 @@ Supports conditions and permission inversion | `delete` | Remove certificate authorities | #### Subject: `certificates` + | Action | Description | | -------- | ----------------------------- | | `read` | View certificates | @@ -229,6 +254,7 @@ Supports conditions and permission inversion | `delete` | Revoke or remove certificates | #### Subject: `certificate-templates` + | Action | Description | | -------- | -------------------------------- | | `read` | View certificate templates | @@ -237,6 +263,7 @@ Supports conditions and permission inversion | `delete` | Remove certificate templates | #### Subject: `pki-alerts` + | Action | Description | | -------- | ------------------------------------------------------------ | | `read` | View PKI alert configurations | @@ -245,6 +272,7 @@ Supports conditions and permission inversion | `delete` | Remove PKI alerts | #### Subject: `pki-collections` + | Action | Description | | -------- | --------------------------------------------------- | | `read` | View PKI resource collections | @@ -255,6 +283,7 @@ Supports conditions and permission inversion ### SSH Certificate Management #### Subject: `ssh-certificate-authorities` + | Action | Description | | -------- | -------------------------------------- | | `read` | View SSH certificate authorities | @@ -263,6 +292,7 @@ Supports conditions and permission inversion | `delete` | Remove SSH certificate authorities | #### Subject: `ssh-certificates` + | Action | Description | | -------- | --------------------------------- | | `read` | View SSH certificates | @@ -271,9 +301,10 @@ Supports conditions and permission inversion | `delete` | Revoke or remove SSH certificates | #### Subject: `ssh-certificate-templates` + | Action | Description | | -------- | ------------------------------------ | | `read` | View SSH certificate templates | | `create` | Create new SSH certificate templates | | `edit` | Modify SSH template configurations | -| `delete` | Remove SSH certificate templates | \ No newline at end of file +| `delete` | Remove SSH certificate templates | diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 2c742db14..1c2de52e2 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -64,7 +64,7 @@ export enum OrgPermissionIdentityActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges", + GrantPrivileges = "grant-privileges", RevokeAuth = "revoke-auth", CreateToken = "create-token", GetToken = "get-token", @@ -76,7 +76,7 @@ export enum OrgPermissionGroupActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges", + GrantPrivileges = "grant-privileges", AddMembers = "add-members", RemoveMembers = "remove-members" } diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 0e09a3a12..fb74e675c 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -56,7 +56,7 @@ export enum ProjectPermissionIdentityActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges" + GrantPrivileges = "grant-privileges" } export enum ProjectPermissionMemberActions { @@ -64,7 +64,7 @@ export enum ProjectPermissionMemberActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges" + GrantPrivileges = "grant-privileges" } export enum ProjectPermissionGroupActions { @@ -72,7 +72,7 @@ export enum ProjectPermissionGroupActions { Create = "create", Edit = "edit", Delete = "delete", - ManagePrivileges = "manage-privileges" + GrantPrivileges = "grant-privileges" } export enum PermissionConditionOperators { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx index be778f71d..2a92089e5 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/UpgradePrivilegeSystemModal/UpgradePrivilegeSystemModal.tsx @@ -1,7 +1,7 @@ +import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; -import { useState } from "react"; import { createNotification } from "@app/components/notifications"; import { Button, Checkbox, Modal, ModalContent } from "@app/components/v2"; @@ -71,94 +71,95 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => {step === "info" ? ( - <> -
-

- We've developed an improved privilege management system that enhances how access controls work in your organization. -

+
+

+ We've developed an improved privilege management system that enhances how access + controls work in your organization. +

-
+
+
-
-

- Current system: - Users with higher privilege levels can modify access for anyone below them. - This rigid hierarchy makes it difficult to implement precise access control policies, - forcing you to either over-grant permissions or create complex workarounds when specialized roles (like team leads) need to manage their team's access without receiving broader administrative powers. -

-

- New system: Users need explicit permission to modify specific - access levels, providing targeted control. After upgrading, you'll need to - grant the new 'Manage Privileges' permission. At the organization level, - this is available for the{" "} - - Group - {" "} - and{" "} - - Identity - {" "} - subjects while at the project level, this is available for the{" "} - - Member, - {" "} - - Group, - {" "} - and{" "} - - Identity - {" "} - subjects. -

-
+

+ Current system: + Users with higher privilege levels can modify access for anyone below them. This + rigid hierarchy makes it difficult to implement precise access control policies, + forcing you to either over-grant permissions or create complex workarounds when + specialized roles (like team leads) need to manage their team's access + without receiving broader administrative powers. +

+

+ New system: Users need explicit permission to modify specific + access levels, providing targeted control. After upgrading, you'll need to + grant the new 'Grant Privileges' permission. At the organization + level, this is available for the{" "} + + Group + {" "} + and{" "} + + Identity + {" "} + subjects while at the project level, this is available for the{" "} + + Member, + {" "} + + Group, + {" "} + and{" "} + + Identity + {" "} + subjects. +

- -
- -
- + +
+ +
+
) : (

- Your existing access control workflows will continue to function. However, actions that - involve modifying privileges or permissions will now use the new permission-based system, - requiring users to have explicit permission to modify given resource. + Your existing access control workflows will continue to function. However, actions + that involve modifying privileges or permissions will now use the new permission-based + system, requiring users to have explicit permission to modify given resource.

@@ -168,9 +169,9 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) =>

- Once upgraded, your organization cannot revert to the - legacy privilege system. Please ensure you've completed all preparations before - proceeding. + Once upgraded, your organization cannot revert to + the legacy privilege system. Please ensure you've completed all preparations + before proceeding.

@@ -233,8 +234,8 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) => onBlur={onBlur} isError={Boolean(error?.message)} > - I have checked Terraform configurations and API integrations for compatibility - with the new system + I have checked Terraform configurations and API integrations for + compatibility with the new system )} /> @@ -265,6 +266,7 @@ export const UpgradePrivilegeSystemModal = ({ isOpen, onOpenChange }: Props) =>