From 943d0ddb6981b92e95a8b7970425e0cc84911ec1 Mon Sep 17 00:00:00 2001 From: Artyom Petrosov Date: Wed, 9 Oct 2024 16:04:21 +0300 Subject: [PATCH 001/107] Helm chart support for tolerations --- .../infisical-standalone-postgres/templates/infisical.yaml | 4 ++++ helm-charts/infisical-standalone-postgres/values.yaml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/helm-charts/infisical-standalone-postgres/templates/infisical.yaml b/helm-charts/infisical-standalone-postgres/templates/infisical.yaml index ac941c9b27..04856406e5 100644 --- a/helm-charts/infisical-standalone-postgres/templates/infisical.yaml +++ b/helm-charts/infisical-standalone-postgres/templates/infisical.yaml @@ -29,6 +29,10 @@ spec: affinity: {{- toYaml . | nindent 8 }} {{- end }} + {{- with $infisicalValues.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} {{- if $infisicalValues.image.imagePullSecrets }} imagePullSecrets: {{- toYaml $infisicalValues.image.imagePullSecrets | nindent 6 }} diff --git a/helm-charts/infisical-standalone-postgres/values.yaml b/helm-charts/infisical-standalone-postgres/values.yaml index e4c1d51b5c..83e6318e04 100644 --- a/helm-charts/infisical-standalone-postgres/values.yaml +++ b/helm-charts/infisical-standalone-postgres/values.yaml @@ -49,6 +49,8 @@ infisical: # -- Node affinity settings for pod placement affinity: {} + # -- Tolerations definitions + tolerations: [] # -- Kubernetes Secret reference containing Infisical root credentials kubeSecretRef: "infisical-secrets" From 596378208e1d37a15c55a0440a010c8d72025d4b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 6 Mar 2025 23:39:39 +0800 Subject: [PATCH 002/107] 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 7de3f8f92a..fdb582bbfb 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 dbabcc2c12..9d0f8c3806 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 80a58db0a5..b7913888a1 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 ff202f225d..ad48223c0f 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 01d013734e..f011654a5c 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 5e404ca20e..9aea92dba4 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 6757b0b846..011a224659 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 a5677894d2..1be1eda3fc 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 ff7256a9cb..bfdaba6d7a 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 bf38c5fa1e..74a47e1a07 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 b9837265aa..9a57d7edae 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 fffcbacc21..d17d6476a0 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 2a31ed6a8a..854230eb05 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 003/107] 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 eb9c66c1c1..6fbc277022 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 d74f9c504f..3b9021b30d 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 4389c38666..1cce302a4d 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 6f87663b2d..f2b37d606b 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 067ff17b0d..000380a8e4 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 36b9b05621..5f14e3ace9 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 fd1382dcf5..18e42dc035 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 004/107] 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 854230eb05..c2873e6f9e 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 42dd314172..56d77ae60f 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 0000000000..eafdd35837 --- /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 0000000000..e1700c17af --- /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 de9fbde802..1816f4292e 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 005/107] 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 1cce302a4d..c5c731dc74 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 d368a949f5..6d274734e7 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 7dcb118fd7..c1dfb51b65 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 006/107] 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 52f8970225..3dcb7588f5 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 b0e5c1f672..182142e07c 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 b56c0a43ea..8ce2569a0a 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 300f05904b..acbbf39022 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 69bcd4f992..5bc163817f 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 fb9d8c3851..51f2797d0a 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 2aa8f27481..7eb59641b0 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 4bdb62d602..cc50e018c8 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 bcebea5c75..0995830e56 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 a2010f43d3..8d844bafc0 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 04b8d01979..90ed8da2a2 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 51d1ecf5b6..467fc7dcc3 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 0000000000..71cfe460f3 --- /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 011/107] 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 3dcb7588f5..d2e8367458 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 03bbbd8eba..37c258dc42 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 513a933bfc..6d603d664a 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 ca46442e99..9b23d6113e 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 3c88fd12d3..a0a94c3525 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 ddaf55d9d5..2997dcf748 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 167ce09645..c6a1ac835f 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 445141540f..37ffacfa54 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 a11fa64ea2..f80b1b2c31 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 2e70138529..56249c6069 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 cf88129808..11c5151e97 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 a3b3842dd2..353d1e46f4 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 4a6467af51..bc7dda22e4 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 f8deb63c7d..bb00776643 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 cabed77510..ae7f8f25b2 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 4dcba69100..ab22164582 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 77e6ad3f37..0c067e057e 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 41252bf255..937fba6f71 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 b89b6f7faf..63aaa24577 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 ecc7f83d0b..62668ed773 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 012/107] 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 d2e8367458..68df7ca699 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 37c258dc42..aaecefb7fc 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 6d603d664a..35e26d942b 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 a0a94c3525..f8f2fa0b82 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 c6a1ac835f..cddc131c96 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 37ffacfa54..05193cc921 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 f80b1b2c31..ddef0c5536 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 56249c6069..61f9ca23fb 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 11c5151e97..014e4619f5 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 353d1e46f4..114ca79c02 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 bc7dda22e4..a18de9ad89 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 bb00776643..3e58c8f6f3 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 ae7f8f25b2..5486b37a8d 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 ab22164582..4079d0756e 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 0c067e057e..c057a656ad 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 937fba6f71..0426e6f8c2 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 63aaa24577..844d3f3858 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 62668ed773..2708e69ba0 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 013/107] 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 f8f2fa0b82..f8a36bcad4 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 844d3f3858..9b4680317f 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 71cfe460f3..aa505965bd 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 014/107] 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 aa505965bd..05deb45d09 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.

*/} Filter project resources + { + e.preventDefault(); + handleToggleRowType(RowType.Import); + }} + icon={filter[RowType.Import] && } + iconPos="right" + > +
+ + Imports +
+
{ e.preventDefault(); @@ -1071,16 +1098,19 @@ export const OverviewPage = () => { key={`overview-${dynamicSecretName}-${index + 1}`} /> ))} - {Object.entries(uniqueEnvSecretPaths).map(([key, secretImportsAllEnvs]) => ( - - ))} + {filter.import && + Object.entries(uniqueEnvSecretPaths).map(([key, secretImportsAllEnvs]) => ( + s ?? []) ?? [] + ).filter(Boolean)} + /> + ))} {secKeys.map((key, index) => ( boolean; scrollOffset: number; - allSecretImports: SecretImportData[][] | undefined; + allSecretImports: TSecretImportMultiEnvData[]; }; export const SecretOverviewImportListView = ({ @@ -27,27 +25,18 @@ export const SecretOverviewImportListView = ({ environments = [], isImportedSecretPresentInEnv, scrollOffset, - allSecretImports + allSecretImports = [] }: Props) => { const [isFormExpanded, setIsFormExpanded] = useToggle(); - const { permission } = useProjectPermission(); const environmentImportDetails = secretImport.environmentInfo; const totalCols = environments.length + 1; - const canReadSecretImports = permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { - environment: environmentImportDetails.slug, - secretPath: secretImport.secretPath - }) - ); - const computeImportedSecrets = - canReadSecretImports && allSecretImports + allSecretImports.length > 0 ? computeImportedSecretRows( environmentImportDetails.slug, secretImport.secretPath, - (allSecretImports?.flatMap((s) => s ?? []) ?? []).filter(Boolean) as SecretImportData[] + allSecretImports ) : []; return ( @@ -55,7 +44,7 @@ export const SecretOverviewImportListView = ({ canReadSecretImports && setIsFormExpanded.toggle()} + onClick={() => setIsFormExpanded.toggle()} className={`group ${isFormExpanded ? "border-t-2 border-mineshaft-500" : ""}`} > @@ -106,7 +95,7 @@ export const SecretOverviewImportListView = ({ ); })} - {canReadSecretImports && isFormExpanded && ( + {isFormExpanded && ( Date: Tue, 18 Mar 2025 16:26:15 -0300 Subject: [PATCH 019/107] Rework of secret imports on overview page --- backend/src/lib/api-docs/constants.ts | 3 +- .../src/server/routes/v1/dashboard-router.ts | 60 ++++++ .../secret-import/secret-import-service.ts | 86 ++++++++- frontend/src/hooks/api/dashboard/queries.tsx | 8 +- frontend/src/hooks/api/dashboard/types.ts | 4 + frontend/src/hooks/api/secretImports/types.ts | 29 +-- frontend/src/hooks/utils/secrets-overview.tsx | 35 ---- .../OverviewPage/OverviewPage.tsx | 75 +++----- .../SecretOverviewImportListView.tsx | 179 ++++++------------ 9 files changed, 243 insertions(+), 236 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 7a350938f7..35ef4e091b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -813,7 +813,8 @@ export const DASHBOARD = { search: "The text string to filter secret keys and folder names by.", includeSecrets: "Whether to include project secrets in the response.", includeFolders: "Whether to include project folders in the response.", - includeDynamicSecrets: "Whether to include dynamic project secrets in the response." + includeDynamicSecrets: "Whether to include dynamic project secrets in the response.", + includeImports: "Whether to include project secret imports in the response." }, SECRET_DETAILS_LIST: { projectId: "The ID of the project to list secrets/folders from.", diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index db61594a2a..11b15f6883 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -109,6 +109,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search: z.string().trim().describe(DASHBOARD.SECRET_OVERVIEW_LIST.search).optional(), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders), + includeImports: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeImports), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets) }), response: { @@ -124,9 +125,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }) .array() .optional(), + imports: SecretImportsSchema.omit({ importEnv: true }) + .extend({ + importEnv: z.object({ name: z.string(), slug: z.string(), id: z.string() }), + environment: z.string() + }) + .array() + .optional(), totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), totalSecretCount: z.number().optional(), + totalImportCount: z.number().optional(), totalCount: z.number() }) } @@ -143,6 +152,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { orderDirection, includeFolders, includeSecrets, + includeImports, includeDynamicSecrets } = req.query; @@ -159,6 +169,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { let remainingLimit = limit; let adjustedOffset = offset; + let imports: Awaited> | undefined; let folders: Awaited> | undefined; let secrets: Awaited> | undefined; let dynamicSecrets: @@ -168,6 +179,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { let totalFolderCount: number | undefined; let totalDynamicSecretCount: number | undefined; let totalSecretCount: number | undefined; + let totalImportCount: number | undefined; + + if (includeImports) { + totalImportCount = await server.services.secretImport.getProjectImportMultiEnvCount({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + environments, + path: secretPath, + search + }); + + if (remainingLimit > 0 && totalImportCount > adjustedOffset) { + imports = await server.services.secretImport.getImportsMultiEnv({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId, + environments, + path: secretPath, + search, + limit: remainingLimit, + offset: adjustedOffset + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_SECRET_IMPORTS, + metadata: { + environment: environments.join(","), + folderId: imports?.[0]?.folderId, + numberOfImports: imports.length + } + } + }); + + remainingLimit -= imports.length; + adjustedOffset = 0; + } else { + adjustedOffset = Math.max(0, adjustedOffset - totalImportCount); + } + } if (includeFolders) { // this is the unique count, ie duplicate folders across envs only count as 1 @@ -345,8 +403,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { folders, dynamicSecrets, secrets, + imports, totalFolderCount, totalDynamicSecretCount, + totalImportCount, totalSecretCount, totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) }; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index b8e8b2fa02..ce03e8e4ed 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -469,6 +469,43 @@ export const secretImportServiceFactory = ({ return count; }; + const getProjectImportMultiEnvCount = async ({ + path: secretPath, + environments, + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + search + }: Omit & { environments: string[] }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + for (const environment of environments) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ); + } + + const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath); + if (!folders?.length) + throw new NotFoundError({ + message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'` + }); + const counts = await Promise.all( + folders.map((folder) => secretImportDAL.getProjectImportCount({ folderId: folder.id, search })) + ); + + return counts.reduce((sum, count) => sum + count, 0); + }; + const getImports = async ({ path: secretPath, environment, @@ -688,6 +725,51 @@ export const secretImportServiceFactory = ({ })); }; + const getImportsMultiEnv = async ({ + path: secretPath, + environments, + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId, + search, + limit, + offset + }: Omit & { environments: string[] }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.SecretManager + }); + for (const environment of environments) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.SecretImports, { environment, secretPath }) + ); + } + + const folders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath); + if (!folders?.length) + throw new NotFoundError({ + message: `Folder with path '${secretPath}' not found on environments with slugs '${environments.join(", ")}'` + }); + + const secImportsArrays = await Promise.all( + folders.map(async (folder) => { + const imports = await secretImportDAL.find({ folderId: folder.id, search, limit, offset }); + return imports.map((importItem) => ({ + ...importItem, + environment: folder.environment.slug + })); + }) + ); + return secImportsArrays.flat(); + }; + return { createImport, updateImport, @@ -698,6 +780,8 @@ export const secretImportServiceFactory = ({ getRawSecretsFromImports, resyncSecretImportReplication, getProjectImportCount, - fnSecretsFromImports + fnSecretsFromImports, + getProjectImportMultiEnvCount, + getImportsMultiEnv }; }; diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index 1fb4563258..ba417430af 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -143,6 +143,7 @@ export const useGetProjectSecretsOverview = ( search = "", includeSecrets, includeFolders, + includeImports, includeDynamicSecrets, environments }: TGetDashboardProjectSecretsOverviewDTO, @@ -170,6 +171,7 @@ export const useGetProjectSecretsOverview = ( projectId, includeSecrets, includeFolders, + includeImports, includeDynamicSecrets, environments }), @@ -184,6 +186,7 @@ export const useGetProjectSecretsOverview = ( projectId, includeSecrets, includeFolders, + includeImports, includeDynamicSecrets, environments }), @@ -197,12 +200,15 @@ export const useGetProjectSecretsOverview = ( ? unique(select.dynamicSecrets, (i) => i.name) : []; + const uniqueSecretImports = select.imports ? unique(select.imports, (i) => i.id) : []; + return { ...select, secrets: secrets ? mergePersonalSecrets(secrets) : undefined, totalUniqueSecretsInPage: uniqueSecrets.length, totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length, - totalUniqueFoldersInPage: uniqueFolders.length + totalUniqueFoldersInPage: uniqueFolders.length, + totalUniqueSecretImportsInPage: uniqueSecretImports.length }; }, []), placeholderData: (previousData) => previousData diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 9540c4ae6b..bdff878cd8 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -9,13 +9,16 @@ export type DashboardProjectSecretsOverviewResponse = { folders?: (TSecretFolder & { environment: string })[]; dynamicSecrets?: (TDynamicSecret & { environment: string })[]; secrets?: SecretV3Raw[]; + imports?: TSecretImport[]; totalSecretCount?: number; totalFolderCount?: number; totalDynamicSecretCount?: number; + totalImportCount?: number; totalCount: number; totalUniqueSecretsInPage: number; totalUniqueDynamicSecretsInPage: number; totalUniqueFoldersInPage: number; + totalUniqueSecretImportsInPage: number; }; export type DashboardProjectSecretsDetailsResponse = { @@ -63,6 +66,7 @@ export type TGetDashboardProjectSecretsOverviewDTO = { includeSecrets?: boolean; includeFolders?: boolean; includeDynamicSecrets?: boolean; + includeImports?: boolean; environments: string[]; }; diff --git a/frontend/src/hooks/api/secretImports/types.ts b/frontend/src/hooks/api/secretImports/types.ts index 7592657c07..d950c2ca26 100644 --- a/frontend/src/hooks/api/secretImports/types.ts +++ b/frontend/src/hooks/api/secretImports/types.ts @@ -14,6 +14,7 @@ export type TSecretImport = { isReplicationSuccess?: boolean; replicationStatus?: string; lastReplicated?: string; + environment?: string; }; export type TGetImportedFoldersByEnvDTO = { @@ -90,31 +91,3 @@ export type TDeleteSecretImportDTO = { environment: string; path?: string; }; - -export type TSecretImportMultiEnvData = { - currentEnv: string; - environment: string; - secretPath: string; - environmentInfo: WorkspaceEnv; - folderId: string; - secrets: { - id: string; - env: string; - key: string; - value: string; - secretValueHidden: boolean; - tags?: { - id: string; - slug: string; - color?: string; - projectId: string; - createdAt: string; - updatedAt: string; - __v: number; - }[]; - comment?: string; - createdAt: string; - updatedAt: string; - version: number; - }[]; -}; diff --git a/frontend/src/hooks/utils/secrets-overview.tsx b/frontend/src/hooks/utils/secrets-overview.tsx index 155893d4be..89fc679c4a 100644 --- a/frontend/src/hooks/utils/secrets-overview.tsx +++ b/frontend/src/hooks/utils/secrets-overview.tsx @@ -1,7 +1,6 @@ import { useCallback, useMemo } from "react"; import { DashboardProjectSecretsOverview } from "@app/hooks/api/dashboard/types"; -import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types"; type FolderNameAndDescription = { name: string; @@ -97,37 +96,3 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr return { secKeys, getSecretByKey, getEnvSecretKeyCount }; }; - -export const useSecretImportOverview = (secretImports: TSecretImportMultiEnvData[] | undefined) => { - const uniqueEnvSecretPaths = useMemo(() => { - const uniqueMap = new Map(); - secretImports?.forEach((importData) => { - if (importData) { - const key = `${importData.environment}-${importData.secretPath}`; - if (!uniqueMap.has(key)) { - uniqueMap.set(key, importData); - } - } - }); - return Array.from(uniqueMap.values()); - }, [secretImports]); - - const isSecretImportPresent = useCallback( - (sourceEnv: string, targetEnv: string, secretPath: string) => { - return ( - secretImports?.some( - (importData) => - importData?.currentEnv === sourceEnv && - importData?.environment === targetEnv && - importData?.secretPath === secretPath - ) ?? false - ); - }, - [secretImports] - ); - - return { - uniqueEnvSecretPaths, - isSecretImportPresent - }; -}; diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 29acae74c2..94ab8c26df 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -68,15 +68,9 @@ import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries"; import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types"; -import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types"; import { SecretType, SecretV3RawSanitized, TSecretFolder } from "@app/hooks/api/types"; import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types"; -import { - useDynamicSecretOverview, - useFolderOverview, - useSecretImportOverview, - useSecretOverview -} from "@app/hooks/utils"; +import { useDynamicSecretOverview, useFolderOverview, useSecretOverview } from "@app/hooks/utils"; import { FolderForm } from "../SecretDashboardPage/components/ActionBar/FolderForm"; import { CreateSecretForm } from "./components/CreateSecretForm"; @@ -195,33 +189,12 @@ export const OverviewPage = () => { setVisibleEnvs(userAvailableEnvs); }, [userAvailableEnvs]); - const { - secretImports, - isImportedSecretPresentInEnv, - getImportedSecretByKey, - getEnvImportedSecretKeyCount - } = useGetImportedSecretsAllEnvs({ - projectId: workspaceId, - path: secretPath, - environments: (userAvailableEnvs || []).map(({ slug }) => slug) - }); - const secretImportsData = useMemo( - () => - (secretImports?.map((s) => s.data as TSecretImportMultiEnvData[]) ?? []) - ?.flatMap((s) => s ?? []) - ?.filter((secretImport) => - permission.can( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.SecretImports, { - environment: secretImport.currentEnv, - secretPath - }) - ) - ), - [secretImports] - ); - const { uniqueEnvSecretPaths, isSecretImportPresent } = - useSecretImportOverview(secretImportsData); + const { isImportedSecretPresentInEnv, getImportedSecretByKey, getEnvImportedSecretKeyCount } = + useGetImportedSecretsAllEnvs({ + projectId: workspaceId, + path: secretPath, + environments: (userAvailableEnvs || []).map(({ slug }) => slug) + }); const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview( { @@ -233,6 +206,7 @@ export const OverviewPage = () => { includeFolders: filter.folder, includeDynamicSecrets: filter.dynamic, includeSecrets: filter.secret, + includeImports: filter.import, search: debouncedSearchFilter, limit, offset @@ -244,15 +218,29 @@ export const OverviewPage = () => { secrets, folders, dynamicSecrets, + imports, totalFolderCount, totalSecretCount, totalDynamicSecretCount, + totalImportCount, totalCount = 0, totalUniqueFoldersInPage, totalUniqueSecretsInPage, + totalUniqueSecretImportsInPage, totalUniqueDynamicSecretsInPage } = overview ?? {}; + const importsShaped = imports + ?.filter((el) => !el.isReserved) + ?.map(({ importPath, importEnv }) => ({ importPath, importEnv })) + .filter( + (el, index, self) => + index === + self.findIndex( + (item) => item.importPath === el.importPath && item.importEnv.slug === el.importEnv.slug + ) + ); + useResetPageHelper({ totalCount, offset, @@ -693,7 +681,6 @@ export const OverviewPage = () => { ); - return (
@@ -1099,16 +1086,14 @@ export const OverviewPage = () => { /> ))} {filter.import && - Object.entries(uniqueEnvSecretPaths).map(([key, secretImportsAllEnvs]) => ( + importsShaped && + importsShaped?.length > 0 && + importsShaped?.map((item, index) => ( s ?? []) ?? [] - ).filter(Boolean)} + key={`overview-secret-input-${index + 1}`} + allSecretImports={imports} /> ))} {secKeys.map((key, index) => ( @@ -1134,7 +1119,8 @@ export const OverviewPage = () => { (page * perPage > totalCount ? totalCount % perPage : perPage) - (totalUniqueFoldersInPage || 0) - (totalUniqueDynamicSecretsInPage || 0) - - (totalUniqueSecretsInPage || 0), + (totalUniqueSecretsInPage || 0) - + (totalUniqueSecretImportsInPage || 0), 0 )} /> @@ -1174,6 +1160,7 @@ export const OverviewPage = () => { dynamicSecretCount={totalDynamicSecretCount} secretCount={totalSecretCount} folderCount={totalFolderCount} + importCount={totalImportCount} /> } className="rounded-b-md border-t border-solid border-t-mineshaft-600" diff --git a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/SecretOverviewImportListView.tsx b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/SecretOverviewImportListView.tsx index fb1982fa78..b92ea6dce0 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/SecretOverviewImportListView.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewImportListView/SecretOverviewImportListView.tsx @@ -1,149 +1,76 @@ -import { faCheck, faFileImport, faKey, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { faCheck, faFileImport, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { EmptyState, SecretInput, TableContainer, Td, Tr } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; -import { TSecretImportMultiEnvData } from "@app/hooks/api/secretImports/types"; +import { Td, Tr } from "@app/components/v2"; +import { TSecretImport, WorkspaceEnv } from "@app/hooks/api/types"; import { EnvFolderIcon } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem"; -import { computeImportedSecretRows } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportListView"; type Props = { - secretImport: TSecretImportMultiEnvData; + secretImport: { importPath: string; importEnv: WorkspaceEnv }; environments: { name: string; slug: string }[]; - isImportedSecretPresentInEnv: ( - sourceEnv: string, - targetEnv: string, - secretPath: string - ) => boolean; - scrollOffset: number; - allSecretImports: TSecretImportMultiEnvData[]; + allSecretImports?: TSecretImport[]; }; export const SecretOverviewImportListView = ({ secretImport, environments = [], - isImportedSecretPresentInEnv, - scrollOffset, allSecretImports = [] }: Props) => { - const [isFormExpanded, setIsFormExpanded] = useToggle(); - const environmentImportDetails = secretImport.environmentInfo; - const totalCols = environments.length + 1; + const isSecretPresentInEnv = (envSlug: string) => { + return allSecretImports.some((item) => { + if (item.isReplication) { + const reservedItem = allSecretImports.find((element) => + element.importPath.includes(`__reserve_replication_${item.id}`) + ); + // If the reserved item exists, check if the envSlug matches + if (reservedItem) { + return reservedItem.environment === envSlug; + } + } else { + // If the item is not replication, check if the envSlug matches directly + return item.environment === envSlug; + } + return false; + }); + }; - const computeImportedSecrets = - allSecretImports.length > 0 - ? computeImportedSecretRows( - environmentImportDetails.slug, - secretImport.secretPath, - allSecretImports - ) - : []; return ( - <> - setIsFormExpanded.toggle()} - className={`group ${isFormExpanded ? "border-t-2 border-mineshaft-500" : ""}`} - > - -
-
- -
-
- -
+ + +
+
+
- - {environments.map(({ slug }, i) => { - const isPresent = isImportedSecretPresentInEnv( - slug, - secretImport.environment, - secretImport.secretPath - ); - - return ( - -
-
- -
-
- - ); - })} - - {isFormExpanded && ( - +
+ +
+
+ + {environments.map(({ slug }, i) => { + const isPresent = isSecretPresentInEnv(slug); + return ( -
- - - - - - - {/* */} - - - - {computeImportedSecrets?.length === 0 && ( - - - - )} - {computeImportedSecrets.map(({ key, value }, index) => ( - - - - - ))} - -
KeyValueOverride
- -
- {key} - - -
-
+
+
+ +
- - )} - + ); + })} + ); }; From a3e9392a2fe4c6e85d87183ee867ce149f267980 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 18 Mar 2025 16:34:31 -0300 Subject: [PATCH 020/107] Fix totalCount missing import count --- backend/src/server/routes/v1/dashboard-router.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 11b15f6883..d976cbc1b1 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -408,7 +408,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalDynamicSecretCount, totalImportCount, totalSecretCount, - totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + totalCount: + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0) }; } }); From dcd3b5df56306f2a73a379d695b9255f2af6ec2d Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Tue, 18 Mar 2025 19:01:52 -0300 Subject: [PATCH 021/107] Add Windmill custom api url domain --- .../integration-auth/integration-app-list.ts | 27 +++++++++---------- .../integration-sync-secret.ts | 24 +++++++---------- .../WindmillAuthorizePage.tsx | 24 +++++++++++++++-- .../WindmillConfigurePage.tsx | 3 ++- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 8b48e20d48..abfa7e5638 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -923,16 +923,14 @@ const getAppsCodefresh = async ({ accessToken }: { accessToken: string }) => { /** * Return list of projects for Windmill integration */ -const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { - const { data } = await request.get<{ id: string; name: string }[]>( - `${IntegrationUrls.WINDMILL_API_URL}/workspaces/list`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } +const getAppsWindmill = async ({ accessToken, url }: { accessToken: string; url?: string }) => { + const apiUrl = url ? `${url}/api` : IntegrationUrls.WINDMILL_API_URL; + const { data } = await request.get<{ id: string; name: string }[]>(`${apiUrl}/workspaces/list`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - ); + }); // check for write access of secrets in windmill workspaces const writeAccessCheck = data.map(async (app) => { @@ -941,7 +939,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { const folderPath = "f/folder/variable"; const { data: writeUser } = await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, + `${apiUrl}/w/${app.id}/variables/create`, { path: userPath, value: "variable", @@ -957,7 +955,7 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { ); const { data: writeFolder } = await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/create`, + `${apiUrl}/w/${app.id}/variables/create`, { path: folderPath, value: "variable", @@ -974,14 +972,14 @@ const getAppsWindmill = async ({ accessToken }: { accessToken: string }) => { // is write access is allowed then delete the created secrets from workspace if (writeUser && writeFolder) { - await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${userPath}`, { + await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${userPath}`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" } }); - await request.delete(`${IntegrationUrls.WINDMILL_API_URL}/w/${app.id}/variables/delete/${folderPath}`, { + await request.delete(`${apiUrl}/w/${app.id}/variables/delete/${folderPath}`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" @@ -1316,7 +1314,8 @@ export const getApps = async ({ case Integrations.WINDMILL: return getAppsWindmill({ - accessToken + accessToken, + url }); case Integrations.DIGITAL_OCEAN_APP_PLATFORM: diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 93318762b5..56e506759b 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -4127,10 +4127,10 @@ const syncSecretsWindmill = async ({ is_secret: boolean; description?: string; } - + const apiUrl = integration.url ? `${integration.url}/api` : IntegrationUrls.WINDMILL_API_URL; // get secrets stored in windmill workspace const res = ( - await request.get(`${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/list`, { + await request.get(`${apiUrl}/w/${integration.appId}/variables/list`, { headers: { Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json" @@ -4146,7 +4146,6 @@ const syncSecretsWindmill = async ({ // eslint-disable-next-line const pattern = new RegExp("^(u/|f/)[a-zA-Z0-9_-]+/([a-zA-Z0-9_-]+/)*[a-zA-Z0-9_-]*[^/]$"); - for await (const key of Object.keys(secrets)) { if ((key.startsWith("u/") || key.startsWith("f/")) && pattern.test(key)) { if (!(key in res)) { @@ -4154,7 +4153,7 @@ const syncSecretsWindmill = async ({ // -> create secret await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/create`, + `${apiUrl}/w/${integration.appId}/variables/create`, { path: key, value: secrets[key].value, @@ -4171,7 +4170,7 @@ const syncSecretsWindmill = async ({ } else { // -> update secret await request.post( - `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/update/${res[key].path}`, + `${apiUrl}/w/${integration.appId}/variables/update/${res[key].path}`, { path: key, value: secrets[key].value, @@ -4192,16 +4191,13 @@ const syncSecretsWindmill = async ({ for await (const key of Object.keys(res)) { if (!(key in secrets)) { // -> delete secret - await request.delete( - `${IntegrationUrls.WINDMILL_API_URL}/w/${integration.appId}/variables/delete/${res[key].path}`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", - "Accept-Encoding": "application/json" - } + await request.delete(`${apiUrl}/w/${integration.appId}/variables/delete/${res[key].path}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json" } - ); + }); } } }; diff --git a/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx b/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx index 0071da9aad..946b5d8e2b 100644 --- a/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/WindmillAuthorizePage/WindmillAuthorizePage.tsx @@ -8,26 +8,34 @@ import { useSaveIntegrationAccessToken } from "@app/hooks/api"; export const WindmillAuthorizePage = () => { const navigate = useNavigate(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const { currentWorkspace } = useWorkspace(); const [apiKey, setApiKey] = useState(""); const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [apiUrl, setApiUrl] = useState(null); + const [apiUrlErrorText, setApiUrlErrorText] = useState(""); const [isLoading, setIsLoading] = useState(false); const handleButtonClick = async () => { try { setApiKeyErrorText(""); + setApiUrlErrorText(""); if (apiKey.length === 0) { setApiKeyErrorText("API Key cannot be blank"); return; } + if (apiUrl && !apiUrl.startsWith("http://") && !apiUrl.startsWith("https://")) { + setApiUrlErrorText("API URL must start with http:// or https://"); + return; + } + setIsLoading(true); const integrationAuth = await mutateAsync({ workspaceId: currentWorkspace.id, integration: "windmill", - accessToken: apiKey + accessToken: apiKey, + url: apiUrl ?? undefined }); setIsLoading(false); @@ -57,6 +65,18 @@ export const WindmillAuthorizePage = () => { > setApiKey(e.target.value)} /> + + setApiUrl(e.target.value.trim() === "" ? null : e.target.value.trim())} + placeholder="https://xxxx.windmill.dev" + /> +